nushell on your web browser
nushell wasm terminal
2
fork

Configure Feed

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

refactor completion.rs into separate files, dont suggest files if argument type isnt filepath or any

dawn 360edbf6 6771fa21

+1931 -1989
+1 -1
src/cmd/ls.rs
··· 22 22 23 23 fn signature(&self) -> Signature { 24 24 Signature::build("ls") 25 - .optional("path", SyntaxShape::String, "the path to list") 25 + .optional("path", SyntaxShape::Filepath, "the path to list") 26 26 .switch( 27 27 "all", 28 28 "include hidden paths (that start with a dot)",
+1 -1
src/cmd/open.rs
··· 18 18 19 19 fn signature(&self) -> Signature { 20 20 Signature::build("open") 21 - .required("path", SyntaxShape::String, "path to the file") 21 + .required("path", SyntaxShape::Filepath, "path to the file") 22 22 .switch( 23 23 "raw", 24 24 "output content as raw string/binary without parsing",
+1 -1
src/cmd/save.rs
··· 16 16 17 17 fn signature(&self) -> Signature { 18 18 Signature::build("save") 19 - .required("path", SyntaxShape::String, "path to write the data to") 19 + .required("path", SyntaxShape::Filepath, "path to write the data to") 20 20 .input_output_types(vec![(Type::Any, Type::Nothing)]) 21 21 .category(Category::FileSystem) 22 22 }
-1984
src/completion.rs
··· 1 - use futures::FutureExt; 2 - use js_sys::Promise; 3 - use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Value}; 4 - use std::collections::HashMap; 5 - use wasm_bindgen_futures::future_to_promise; 6 - 7 - use super::*; 8 - 9 - #[derive(Debug, Serialize)] 10 - struct Suggestion { 11 - name: String, 12 - description: Option<String>, 13 - is_command: bool, 14 - rendered: String, 15 - span_start: usize, // char index (not byte) 16 - span_end: usize, // char index (not byte) 17 - } 18 - 19 - impl PartialEq for Suggestion { 20 - fn eq(&self, other: &Self) -> bool { 21 - self.name == other.name 22 - } 23 - } 24 - impl Eq for Suggestion {} 25 - impl PartialOrd for Suggestion { 26 - fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 27 - self.name.partial_cmp(&other.name) 28 - } 29 - } 30 - impl Ord for Suggestion { 31 - fn cmp(&self, other: &Self) -> std::cmp::Ordering { 32 - self.name.cmp(&other.name) 33 - } 34 - } 35 - 36 - #[wasm_bindgen] 37 - pub fn completion(input: String, js_cursor_pos: usize) -> Promise { 38 - future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s)))) 39 - } 40 - 41 - pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { 42 - let engine_guard = read_engine_state().await; 43 - let stack_guard = crate::read_stack().await; 44 - let root = get_pwd(); 45 - 46 - // Map UTF-16 cursor position (from JS) to Byte index (for Rust) 47 - let byte_pos = input 48 - .char_indices() 49 - .map(|(i, _)| i) 50 - .nth(js_cursor_pos) 51 - .unwrap_or(input.len()); 52 - 53 - let (working_set, shapes, global_offset) = { 54 - let mut working_set = StateWorkingSet::new(&engine_guard); 55 - let global_offset = working_set.next_span_start(); 56 - let block = parse(&mut working_set, None, input.as_bytes(), false); 57 - let shapes = flatten_block(&working_set, &block); 58 - (working_set, shapes, global_offset) 59 - }; 60 - 61 - // Initial state logging 62 - web_sys::console::log_1(&JsValue::from_str(&format!( 63 - "[completion] Input: {:?}, JS cursor: {}, byte cursor: {}", 64 - input, js_cursor_pos, byte_pos 65 - ))); 66 - web_sys::console::log_1(&JsValue::from_str(&format!( 67 - "[completion] Found {} shapes, global_offset: {}", 68 - shapes.len(), 69 - global_offset 70 - ))); 71 - for (idx, (span, shape)) in shapes.iter().enumerate() { 72 - let (local_start, local_end) = ( 73 - span.start.saturating_sub(global_offset), 74 - span.end.saturating_sub(global_offset), 75 - ); 76 - web_sys::console::log_1(&JsValue::from_str(&format!( 77 - "[completion] Shape {}: {:?} at [{}, {}] (local: [{}, {}])", 78 - idx, shape, span.start, span.end, local_start, local_end 79 - ))); 80 - } 81 - 82 - // Helper functions 83 - let is_separator_char = |c: char| -> bool { ['|', ';', '(', '{'].contains(&c) }; 84 - 85 - let is_command_separator_char = |c: char| -> bool { ['|', ';'].contains(&c) }; 86 - 87 - let has_separator_between = |start: usize, end: usize| -> bool { 88 - if start < end && start < input.len() { 89 - let text_between = &input[start..std::cmp::min(end, input.len())]; 90 - text_between.chars().any(|c| is_separator_char(c)) 91 - } else { 92 - false 93 - } 94 - }; 95 - 96 - let find_last_separator_pos = |text: &str| -> Option<usize> { 97 - text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1) 98 - }; 99 - 100 - let ends_with_separator = |text: &str| -> bool { 101 - let text = text.trim_end(); 102 - text.ends_with('|') || text.ends_with(';') 103 - }; 104 - 105 - let to_local_span = |span: Span| -> Span { 106 - Span::new( 107 - span.start.saturating_sub(global_offset), 108 - span.end.saturating_sub(global_offset), 109 - ) 110 - }; 111 - 112 - let safe_slice = |span: Span| -> String { 113 - (span.start < input.len()) 114 - .then(|| { 115 - let safe_end = std::cmp::min(span.end, input.len()); 116 - input[span.start..safe_end].to_string() 117 - }) 118 - .unwrap_or_default() 119 - }; 120 - 121 - let is_command_shape = |shape: &FlatShape, local_span: Span| -> bool { 122 - matches!( 123 - shape, 124 - FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword 125 - ) || matches!(shape, FlatShape::Garbage) && { 126 - if local_span.start < input.len() { 127 - let prev_text = &safe_slice(local_span); 128 - !prev_text.trim().starts_with('-') 129 - } else { 130 - false 131 - } 132 - } 133 - }; 134 - 135 - let handle_block_prefix = |prefix: &str, span: Span| -> Option<(String, Span, bool)> { 136 - let mut block_prefix = prefix; 137 - let mut block_span_start = span.start; 138 - 139 - // Remove leading '{' and whitespace 140 - if block_prefix.starts_with('{') { 141 - block_prefix = &block_prefix[1..]; 142 - block_span_start += 1; 143 - } 144 - let trimmed_block_prefix = block_prefix.trim_start(); 145 - if trimmed_block_prefix != block_prefix { 146 - // Adjust span start to skip whitespace 147 - block_span_start += block_prefix.len() - trimmed_block_prefix.len(); 148 - } 149 - 150 - let is_empty = trimmed_block_prefix.is_empty(); 151 - Some(( 152 - trimmed_block_prefix.to_string(), 153 - Span::new(block_span_start, span.end), 154 - is_empty, 155 - )) 156 - }; 157 - 158 - // Helper function to find command name and count arguments before cursor 159 - let find_command_and_arg_index = 160 - |current_idx: usize, current_local_span: Span| -> Option<(String, usize)> { 161 - let mut command_name: Option<String> = None; 162 - let mut arg_count = 0; 163 - 164 - // Look backwards through shapes to find the command 165 - for i in (0..current_idx).rev() { 166 - if let Some((prev_span, prev_shape)) = shapes.get(i) { 167 - let prev_local_span = to_local_span(*prev_span); 168 - 169 - // Check if there's a separator between this shape and the next one 170 - let next_shape_start = if i + 1 < shapes.len() { 171 - to_local_span(shapes[i + 1].0).start 172 - } else { 173 - current_local_span.start 174 - }; 175 - 176 - if has_separator_between(prev_local_span.end, next_shape_start) { 177 - break; // Stop at separator 178 - } 179 - 180 - if is_command_shape(prev_shape, prev_local_span) { 181 - // Found the command 182 - let cmd_text = safe_slice(prev_local_span); 183 - // Extract just the command name (first word, no flags) 184 - let cmd_name = cmd_text 185 - .split_whitespace() 186 - .next() 187 - .unwrap_or(&cmd_text) 188 - .trim(); 189 - command_name = Some(cmd_name.to_string()); 190 - break; 191 - } else { 192 - // This is an argument - count it if it's not a flag 193 - let arg_text = safe_slice(prev_local_span); 194 - let trimmed_arg = arg_text.trim(); 195 - // Don't count flags (starting with -) or empty arguments 196 - if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 197 - arg_count += 1; 198 - } 199 - } 200 - } 201 - } 202 - 203 - command_name.map(|name| (name, arg_count)) 204 - }; 205 - 206 - // Helper function to handle both Block and Closure shapes 207 - let handle_block_or_closure = |prefix: &str, 208 - span: Span, 209 - shape_name: &str, 210 - current_idx: usize, 211 - local_span: Span| 212 - -> Option<CompletionContext> { 213 - web_sys::console::log_1(&JsValue::from_str(&format!( 214 - "[completion] Processing {} shape with prefix: {:?}", 215 - shape_name, prefix 216 - ))); 217 - 218 - // Check if the content ends with a pipe or semicolon 219 - let prefix_ends_with_separator = ends_with_separator(prefix); 220 - let last_sep_pos_in_prefix = if prefix_ends_with_separator { 221 - find_last_separator_pos(prefix) 222 - } else { 223 - None 224 - }; 225 - web_sys::console::log_1(&JsValue::from_str(&format!( 226 - "[completion] {}: prefix_ends_with_separator={}, last_sep_pos_in_prefix={:?}", 227 - shape_name, prefix_ends_with_separator, last_sep_pos_in_prefix 228 - ))); 229 - 230 - if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) { 231 - web_sys::console::log_1(&JsValue::from_str(&format!( 232 - "[completion] {}: trimmed_prefix={:?}, is_empty={}", 233 - shape_name, trimmed_prefix, is_empty 234 - ))); 235 - 236 - if is_empty { 237 - // Empty block/closure or just whitespace - command context 238 - web_sys::console::log_1(&JsValue::from_str(&format!( 239 - "[completion] {} is empty, setting Command context", 240 - shape_name 241 - ))); 242 - Some(CompletionContext::Command { 243 - prefix: String::new(), 244 - span: adjusted_span, 245 - }) 246 - } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { 247 - // After a separator - command context 248 - let after_sep = prefix[last_sep_pos..].trim_start(); 249 - web_sys::console::log_1(&JsValue::from_str(&format!( 250 - "[completion] {} has separator at {}, after_sep={:?}, setting Command context", 251 - shape_name, last_sep_pos, after_sep 252 - ))); 253 - Some(CompletionContext::Command { 254 - prefix: after_sep.to_string(), 255 - span: Span::new(span.start + last_sep_pos, span.end), 256 - }) 257 - } else { 258 - web_sys::console::log_1(&JsValue::from_str(&format!( 259 - "[completion] {} has no separator, checking for variable/flag/argument context", 260 - shape_name 261 - ))); 262 - // Check if this is a variable or cell path first 263 - let trimmed = trimmed_prefix.trim(); 264 - 265 - if trimmed.starts_with('$') { 266 - // Variable or cell path completion 267 - if let Some(dot_pos) = trimmed[1..].find('.') { 268 - // Cell path completion: $in.name, $env.PWD, etc. 269 - let var_name = &trimmed[1..dot_pos + 1]; 270 - let after_var = &trimmed[dot_pos + 2..]; 271 - let parts: Vec<&str> = after_var.split('.').collect(); 272 - let (path_so_far, cell_prefix) = if parts.is_empty() { 273 - (vec![], String::new()) 274 - } else if after_var.ends_with('.') { 275 - ( 276 - parts 277 - .iter() 278 - .filter(|s| !s.is_empty()) 279 - .map(|s| s.to_string()) 280 - .collect(), 281 - String::new(), 282 - ) 283 - } else { 284 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 285 - .iter() 286 - .map(|s| s.to_string()) 287 - .collect(); 288 - let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 289 - (path, prefix) 290 - }; 291 - 292 - let var_id = match var_name { 293 - "env" => Some(ENV_VARIABLE_ID), 294 - "nu" => Some(NU_VARIABLE_ID), 295 - "in" => Some(IN_VARIABLE_ID), 296 - _ => working_set.find_variable(var_name.as_bytes()), 297 - }; 298 - 299 - if let Some(var_id) = var_id { 300 - let prefix_byte_len = cell_prefix.len(); 301 - let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); 302 - web_sys::console::log_1(&JsValue::from_str(&format!( 303 - "[completion] {}: Setting CellPath context with var {:?}, prefix {:?}", 304 - shape_name, var_name, cell_prefix 305 - ))); 306 - Some(CompletionContext::CellPath { 307 - prefix: cell_prefix, 308 - span: Span::new(cell_span_start, adjusted_span.end), 309 - var_id, 310 - path_so_far, 311 - }) 312 - } else { 313 - // Unknown variable, fall back to variable completion 314 - let var_prefix = trimmed[1..].to_string(); 315 - web_sys::console::log_1(&JsValue::from_str(&format!( 316 - "[completion] {}: Unknown var, setting Variable context with prefix {:?}", 317 - shape_name, var_prefix 318 - ))); 319 - Some(CompletionContext::Variable { 320 - prefix: var_prefix, 321 - span: adjusted_span, 322 - }) 323 - } 324 - } else { 325 - // Simple variable completion (no dot) 326 - let var_prefix = if trimmed.len() > 1 { 327 - trimmed[1..].to_string() 328 - } else { 329 - String::new() 330 - }; 331 - web_sys::console::log_1(&JsValue::from_str(&format!( 332 - "[completion] {}: Setting Variable context with prefix {:?}", 333 - shape_name, var_prefix 334 - ))); 335 - Some(CompletionContext::Variable { 336 - prefix: var_prefix, 337 - span: adjusted_span, 338 - }) 339 - } 340 - } else if trimmed.starts_with('-') { 341 - // Flag completion 342 - if let Some((cmd_name, _)) = find_command_and_arg_index(current_idx, local_span) 343 - { 344 - web_sys::console::log_1(&JsValue::from_str(&format!( 345 - "[completion] {}: Found command {:?} for flag completion", 346 - shape_name, cmd_name 347 - ))); 348 - Some(CompletionContext::Flag { 349 - prefix: trimmed.to_string(), 350 - span: adjusted_span, 351 - command_name: cmd_name, 352 - }) 353 - } else { 354 - Some(CompletionContext::Argument { 355 - prefix: trimmed_prefix, 356 - span: adjusted_span, 357 - }) 358 - } 359 - } else { 360 - // Try to find the command and argument index 361 - if let Some((cmd_name, arg_index)) = 362 - find_command_and_arg_index(current_idx, local_span) 363 - { 364 - web_sys::console::log_1(&JsValue::from_str(&format!( 365 - "[completion] {}: Found command {:?} with arg_index {} for argument completion", 366 - shape_name, cmd_name, arg_index 367 - ))); 368 - Some(CompletionContext::CommandArgument { 369 - prefix: trimmed.to_string(), 370 - span: adjusted_span, 371 - command_name: cmd_name, 372 - arg_index, 373 - }) 374 - } else { 375 - // No command found, treat as regular argument 376 - web_sys::console::log_1(&JsValue::from_str(&format!( 377 - "[completion] {}: No command found, using Argument context", 378 - shape_name 379 - ))); 380 - Some(CompletionContext::Argument { 381 - prefix: trimmed_prefix, 382 - span: adjusted_span, 383 - }) 384 - } 385 - } 386 - } 387 - } else { 388 - None 389 - } 390 - }; 391 - 392 - // Helper function to evaluate a variable for completion 393 - // Returns the Value of a variable if it can be evaluated 394 - let eval_variable_for_completion = |var_id: nu_protocol::VarId, 395 - working_set: &StateWorkingSet| 396 - -> Option<Value> { 397 - match var_id { 398 - id if id == NU_VARIABLE_ID => { 399 - // $nu - get from engine state constant 400 - engine_guard.get_constant(id).cloned() 401 - } 402 - id if id == ENV_VARIABLE_ID => { 403 - // $env - build from environment variables in engine state 404 - // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars) 405 - let mut pairs: Vec<(String, Value)> = Vec::new(); 406 - for overlay_env in engine_guard.env_vars.values() { 407 - for (name, value) in overlay_env.iter() { 408 - pairs.push((name.clone(), value.clone())); 409 - } 410 - } 411 - pairs.sort_by(|a, b| a.0.cmp(&b.0)); 412 - // Deduplicate by name (later overlays override earlier ones) 413 - pairs.dedup_by(|a, b| a.0 == b.0); 414 - Some(Value::record(pairs.into_iter().collect(), Span::unknown())) 415 - } 416 - id if id == IN_VARIABLE_ID => { 417 - // $in - typically not available at completion time 418 - None 419 - } 420 - _ => { 421 - // User-defined variable - try to get const value first 422 - let var_info = working_set.get_variable(var_id); 423 - if let Some(const_val) = &var_info.const_val { 424 - Some(const_val.clone()) 425 - } else { 426 - // Variable doesn't have a const value (runtime value) 427 - // Try to get the value from the stack (runtime storage) 428 - match stack_guard.get_var(var_id, Span::unknown()) { 429 - Ok(value) => { 430 - web_sys::console::log_1(&JsValue::from_str(&format!( 431 - "[completion] Found variable {:?} value in stack", 432 - var_id 433 - ))); 434 - Some(value) 435 - } 436 - Err(_) => { 437 - // Variable not in stack either 438 - web_sys::console::log_1(&JsValue::from_str(&format!( 439 - "[completion] Variable {:?} has no const value and not in stack, type: {:?}", 440 - var_id, var_info.ty 441 - ))); 442 - None 443 - } 444 - } 445 - } 446 - } 447 - } 448 - }; 449 - 450 - // Helper function to extract column/field names from a Value 451 - let get_columns_from_value = |value: &Value| -> Vec<(String, Option<String>)> { 452 - match value { 453 - Value::Record { val, .. } => val 454 - .iter() 455 - .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 456 - .collect(), 457 - Value::List { vals, .. } => { 458 - // Get common columns from list of records 459 - if let Some(first) = vals.first() { 460 - if let Value::Record { val, .. } = first { 461 - return val 462 - .iter() 463 - .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 464 - .collect(); 465 - } 466 - } 467 - vec![] 468 - } 469 - _ => vec![], 470 - } 471 - }; 472 - 473 - // Helper function to follow a cell path and get the value at that path 474 - let follow_cell_path = |value: &Value, path: &[String]| -> Option<Value> { 475 - let mut current = value.clone(); 476 - for member in path { 477 - match &current { 478 - Value::Record { val, .. } => { 479 - current = val.get(member)?.clone(); 480 - } 481 - Value::List { vals, .. } => { 482 - // Try to parse as index or get from first record 483 - if let Ok(idx) = member.parse::<usize>() { 484 - current = vals.get(idx)?.clone(); 485 - } else if let Some(first) = vals.first() { 486 - if let Value::Record { val, .. } = first { 487 - current = val.get(member)?.clone(); 488 - } else { 489 - return None; 490 - } 491 - } else { 492 - return None; 493 - } 494 - } 495 - _ => return None, 496 - } 497 - } 498 - Some(current) 499 - }; 500 - 501 - // Helper function to extract closure parameters from input string at cursor position 502 - // We parse the input directly to find closures containing the cursor and extract their parameters 503 - let extract_closure_params = |input: &str, cursor_pos: usize| -> Vec<String> { 504 - let mut params = Vec::new(); 505 - 506 - // Find all closures in the input by looking for {|...| patterns 507 - // We need to find closures that contain the cursor position 508 - let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions 509 - let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params) 510 - 511 - let mut i = 0; 512 - let chars: Vec<char> = input.chars().collect(); 513 - 514 - while i < chars.len() { 515 - if chars[i] == '{' { 516 - brace_stack.push(i); 517 - } else if chars[i] == '}' { 518 - if let Some(start) = brace_stack.pop() { 519 - // Check if this is a closure with parameters: {|param| ...} 520 - if start + 1 < chars.len() && chars[start + 1] == '|' { 521 - // Find the parameter list 522 - let param_start = start + 2; 523 - let mut param_end = param_start; 524 - 525 - // Find the closing | of the parameter list 526 - while param_end < chars.len() && chars[param_end] != '|' { 527 - param_end += 1; 528 - } 529 - 530 - if param_end < chars.len() { 531 - // Extract parameter names 532 - let params_text: String = 533 - chars[param_start..param_end].iter().collect(); 534 - let param_names: Vec<String> = params_text 535 - .split(',') 536 - .map(|s| s.trim().to_string()) 537 - .filter(|s| !s.is_empty()) 538 - .collect(); 539 - 540 - closures.push((start, i + 1, param_names)); 541 - } 542 - } 543 - } 544 - } 545 - i += 1; 546 - } 547 - 548 - // Find closures that contain the cursor position 549 - // A closure contains the cursor if: start <= cursor_pos < end 550 - for (start, end, param_names) in closures { 551 - if start <= cursor_pos && cursor_pos < end { 552 - web_sys::console::log_1(&JsValue::from_str(&format!( 553 - "[completion] Found closure at [{}, {}) containing cursor {}, params: {:?}", 554 - start, end, cursor_pos, param_names 555 - ))); 556 - params.extend(param_names); 557 - } 558 - } 559 - 560 - params 561 - }; 562 - 563 - // Helper function to collect variables from working set 564 - let collect_variables = |working_set: &StateWorkingSet, 565 - input: &str, 566 - cursor_pos: usize| 567 - -> HashMap<String, nu_protocol::VarId> { 568 - let mut variables = HashMap::new(); 569 - 570 - // Add built-in variables 571 - variables.insert("$nu".to_string(), NU_VARIABLE_ID); 572 - variables.insert("$in".to_string(), IN_VARIABLE_ID); 573 - variables.insert("$env".to_string(), ENV_VARIABLE_ID); 574 - 575 - // Collect closure parameters at cursor position 576 - // We don't need real var_ids for closure parameters since they're not evaluated yet 577 - // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder) 578 - // The actual var_id lookup will happen when the variable is used 579 - let closure_params = extract_closure_params(input, cursor_pos); 580 - for param_name in closure_params { 581 - let var_name = format!("${}", param_name); 582 - // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name 583 - // The completion logic only needs the name, not the actual var_id 584 - variables.insert(var_name.clone(), IN_VARIABLE_ID); 585 - web_sys::console::log_1(&JsValue::from_str(&format!( 586 - "[completion] Added closure parameter: {:?}", 587 - var_name 588 - ))); 589 - } 590 - 591 - // Collect from working set delta scope 592 - let mut removed_overlays = vec![]; 593 - for scope_frame in working_set.delta.scope.iter().rev() { 594 - for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { 595 - for (name, var_id) in &overlay_frame.vars { 596 - let name = String::from_utf8_lossy(name).to_string(); 597 - variables.insert(name, *var_id); 598 - } 599 - } 600 - } 601 - 602 - // Collect from permanent state scope 603 - for overlay_frame in working_set 604 - .permanent_state 605 - .active_overlays(&removed_overlays) 606 - .rev() 607 - { 608 - for (name, var_id) in &overlay_frame.vars { 609 - let name = String::from_utf8_lossy(name).to_string(); 610 - variables.insert(name, *var_id); 611 - } 612 - } 613 - 614 - variables 615 - }; 616 - 617 - // Find what we're completing 618 - #[derive(Debug)] 619 - enum CompletionContext { 620 - Command { 621 - prefix: String, 622 - span: Span, 623 - }, 624 - Argument { 625 - prefix: String, 626 - span: Span, 627 - }, 628 - Flag { 629 - prefix: String, 630 - span: Span, 631 - command_name: String, 632 - }, 633 - CommandArgument { 634 - prefix: String, 635 - span: Span, 636 - command_name: String, 637 - arg_index: usize, 638 - }, 639 - Variable { 640 - prefix: String, // without the $ prefix 641 - span: Span, 642 - }, 643 - CellPath { 644 - prefix: String, // the partial field name being typed (after the last dot) 645 - span: Span, // replacement span 646 - var_id: nu_protocol::VarId, // variable ID for evaluation 647 - path_so_far: Vec<String>, // path members accessed before current one 648 - }, 649 - } 650 - 651 - let mut context: Option<CompletionContext> = None; 652 - 653 - // Helper function to build full command prefix by looking backwards through shapes 654 - let build_command_prefix = 655 - |current_idx: usize, current_local_span: Span, current_prefix: &str| -> (String, Span) { 656 - let mut span_start = current_local_span.start; 657 - 658 - // Look backwards through shapes to find previous command words 659 - for i in (0..current_idx).rev() { 660 - if let Some((prev_span, prev_shape)) = shapes.get(i) { 661 - let prev_local_span = to_local_span(*prev_span); 662 - 663 - if is_command_shape(prev_shape, prev_local_span) { 664 - // Check if there's a separator between this shape and the next one 665 - let next_shape_start = if i + 1 < shapes.len() { 666 - to_local_span(shapes[i + 1].0).start 667 - } else { 668 - current_local_span.start 669 - }; 670 - 671 - // Check if there's a separator (pipe, semicolon, etc.) between shapes 672 - // Whitespace is fine, but separators indicate a new command 673 - if has_separator_between(prev_local_span.end, next_shape_start) { 674 - break; // Stop at separator 675 - } 676 - 677 - // Update span start to include this command word 678 - span_start = prev_local_span.start; 679 - } else { 680 - // Not a command shape, stop looking backwards 681 - break; 682 - } 683 - } 684 - } 685 - 686 - // Extract the full prefix from the input, preserving exact spacing 687 - let span_end = current_local_span.end; 688 - let full_prefix = if span_start < input.len() { 689 - safe_slice(Span::new(span_start, span_end)) 690 - } else { 691 - current_prefix.to_string() 692 - }; 693 - 694 - (full_prefix, Span::new(span_start, span_end)) 695 - }; 696 - 697 - // Helper function to get command signature (needed for context determination) 698 - let get_command_signature = |cmd_name: &str| -> Option<nu_protocol::Signature> { 699 - engine_guard 700 - .find_decl(cmd_name.as_bytes(), &[]) 701 - .map(|id| engine_guard.get_decl(id).signature()) 702 - }; 703 - 704 - // First, check if cursor is within a shape 705 - for (idx, (span, shape)) in shapes.iter().enumerate() { 706 - let local_span = to_local_span(*span); 707 - 708 - if local_span.start <= byte_pos && byte_pos <= local_span.end { 709 - web_sys::console::log_1(&JsValue::from_str(&format!( 710 - "[completion] Cursor in shape {}: {:?} at {:?}", 711 - idx, shape, local_span 712 - ))); 713 - 714 - // Check if there's a pipe or semicolon between this shape's end and the cursor 715 - // If so, we're starting a new command and should ignore this shape 716 - let has_sep = has_separator_between(local_span.end, byte_pos); 717 - if has_sep { 718 - web_sys::console::log_1(&JsValue::from_str(&format!( 719 - "[completion] Separator found between shape end ({}) and cursor ({}), skipping shape", 720 - local_span.end, byte_pos 721 - ))); 722 - // There's a separator, so we're starting a new command - skip this shape 723 - continue; 724 - } 725 - 726 - let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos)); 727 - let prefix = safe_slice(span); 728 - web_sys::console::log_1(&JsValue::from_str(&format!( 729 - "[completion] Processing shape {} with prefix: {:?}", 730 - idx, prefix 731 - ))); 732 - 733 - // Special case: if prefix is just '{' (possibly with whitespace), 734 - // we're at the start of a block and should complete commands 735 - let trimmed_prefix = prefix.trim(); 736 - if trimmed_prefix == "{" { 737 - // We're right after '{' - command context 738 - if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) { 739 - context = Some(CompletionContext::Command { 740 - prefix: String::new(), 741 - span: adjusted_span, 742 - }); 743 - } 744 - } else { 745 - match shape { 746 - // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes 747 - // e.g., `$a.na` where $a is a Variable shape and `.na` is a String shape 748 - _ if { idx > 0 && matches!(shape, FlatShape::String) } => { 749 - // Look at the previous shape to see if it's a Variable 750 - let prev_shape = &shapes[idx - 1]; 751 - let prev_local_span = to_local_span(prev_shape.0); 752 - 753 - if let FlatShape::Variable(var_id) = prev_shape.1 { 754 - // Check if the variable shape ends right where this shape starts (or very close) 755 - // Allow for a small gap (like a dot) between shapes 756 - let gap = local_span.start.saturating_sub(prev_local_span.end); 757 - if gap <= 1 { 758 - // This is a cell path - the String shape contains the field name(s) 759 - // The prefix might be like "na" or "field.subfield" 760 - let trimmed_prefix = prefix.trim(); 761 - let parts: Vec<&str> = trimmed_prefix.split('.').collect(); 762 - let (path_so_far, cell_prefix) = if parts.is_empty() { 763 - (vec![], String::new()) 764 - } else if trimmed_prefix.ends_with('.') { 765 - ( 766 - parts 767 - .iter() 768 - .filter(|s| !s.is_empty()) 769 - .map(|s| s.to_string()) 770 - .collect(), 771 - String::new(), 772 - ) 773 - } else { 774 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 775 - .iter() 776 - .map(|s| s.to_string()) 777 - .collect(); 778 - let prefix = 779 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 780 - (path, prefix) 781 - }; 782 - 783 - let prefix_byte_len = cell_prefix.len(); 784 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 785 - web_sys::console::log_1(&JsValue::from_str(&format!( 786 - "[completion] Detected cell path from Variable+String shapes, var_id={:?}, prefix={:?}, path={:?}", 787 - var_id, cell_prefix, path_so_far 788 - ))); 789 - context = Some(CompletionContext::CellPath { 790 - prefix: cell_prefix, 791 - span: Span::new(cell_span_start, span.end), 792 - var_id, 793 - path_so_far, 794 - }); 795 - } else { 796 - // Gap between shapes, check if this is a flag 797 - let trimmed_prefix = prefix.trim(); 798 - if trimmed_prefix.starts_with('-') { 799 - // This looks like a flag - find the command 800 - if let Some((cmd_name, _)) = 801 - find_command_and_arg_index(idx, local_span) 802 - { 803 - context = Some(CompletionContext::Flag { 804 - prefix: trimmed_prefix.to_string(), 805 - span, 806 - command_name: cmd_name, 807 - }); 808 - } else { 809 - context = 810 - Some(CompletionContext::Argument { prefix, span }); 811 - } 812 - } else { 813 - context = Some(CompletionContext::Argument { prefix, span }); 814 - } 815 - } 816 - } else { 817 - // Previous shape is not a Variable, check if this is a flag 818 - let trimmed_prefix = prefix.trim(); 819 - if trimmed_prefix.starts_with('-') { 820 - // This looks like a flag - find the command 821 - if let Some((cmd_name, _)) = 822 - find_command_and_arg_index(idx, local_span) 823 - { 824 - context = Some(CompletionContext::Flag { 825 - prefix: trimmed_prefix.to_string(), 826 - span, 827 - command_name: cmd_name, 828 - }); 829 - } else { 830 - context = Some(CompletionContext::Argument { prefix, span }); 831 - } 832 - } else { 833 - // This is likely a regular string argument 834 - context = Some(CompletionContext::Argument { prefix, span }); 835 - } 836 - } 837 - } 838 - // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes 839 - // e.g., `{ $in. }` where $in is Shape 4 (Variable) and `. }` is Shape 5 (Block) 840 - _ if { 841 - let trimmed_prefix = prefix.trim(); 842 - trimmed_prefix.starts_with('.') && idx > 0 843 - } => 844 - { 845 - // Look at the previous shape to see if it's a Variable 846 - let prev_shape = &shapes[idx - 1]; 847 - let prev_local_span = to_local_span(prev_shape.0); 848 - 849 - if let FlatShape::Variable(var_id) = prev_shape.1 { 850 - // Check if the variable shape ends right where this shape starts 851 - if prev_local_span.end == local_span.start { 852 - let trimmed_prefix = prefix.trim(); 853 - // Parse path members from the prefix (which is like ".field" or ".field.subfield") 854 - let after_dot = &trimmed_prefix[1..]; // Remove leading dot 855 - let parts: Vec<&str> = after_dot.split('.').collect(); 856 - let (path_so_far, cell_prefix) = if parts.is_empty() 857 - || (parts.len() == 1 && parts[0].is_empty()) 858 - { 859 - (vec![], String::new()) 860 - } else if after_dot.ends_with('.') { 861 - ( 862 - parts 863 - .iter() 864 - .filter(|s| !s.is_empty()) 865 - .map(|s| s.to_string()) 866 - .collect(), 867 - String::new(), 868 - ) 869 - } else { 870 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 871 - .iter() 872 - .map(|s| s.to_string()) 873 - .collect(); 874 - let prefix = 875 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 876 - (path, prefix) 877 - }; 878 - 879 - let prefix_byte_len = cell_prefix.len(); 880 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 881 - web_sys::console::log_1(&JsValue::from_str(&format!( 882 - "[completion] Detected cell path from adjacent Variable shape, var_id={:?}, prefix={:?}", 883 - var_id, cell_prefix 884 - ))); 885 - context = Some(CompletionContext::CellPath { 886 - prefix: cell_prefix, 887 - span: Span::new(cell_span_start, span.end), 888 - var_id, 889 - path_so_far, 890 - }); 891 - } else { 892 - // Gap between shapes, fall through to default handling 893 - context = Some(CompletionContext::Argument { prefix, span }); 894 - } 895 - } else { 896 - // Previous shape is not a Variable, this is likely a file path starting with . 897 - context = Some(CompletionContext::Argument { prefix, span }); 898 - } 899 - } 900 - _ if { 901 - // Check if this is a variable or cell path (starts with $) before treating as command 902 - let trimmed_prefix = prefix.trim(); 903 - trimmed_prefix.starts_with('$') 904 - } => 905 - { 906 - let trimmed_prefix = prefix.trim(); 907 - // Check if this is a cell path (contains a dot after $) 908 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 909 - // Cell path completion: $env.PWD, $nu.home-path, etc. 910 - let var_name = &trimmed_prefix[1..dot_pos + 1]; // e.g., "env" 911 - let after_var = &trimmed_prefix[dot_pos + 2..]; // e.g., "PWD" or "config.color" 912 - 913 - // Parse path members and current prefix 914 - let parts: Vec<&str> = after_var.split('.').collect(); 915 - let (path_so_far, cell_prefix) = if parts.is_empty() { 916 - (vec![], String::new()) 917 - } else if after_var.ends_with('.') { 918 - // Cursor is right after a dot, complete all fields 919 - ( 920 - parts 921 - .iter() 922 - .filter(|s| !s.is_empty()) 923 - .map(|s| s.to_string()) 924 - .collect(), 925 - String::new(), 926 - ) 927 - } else { 928 - // Cursor is in the middle of typing a field name 929 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 930 - .iter() 931 - .map(|s| s.to_string()) 932 - .collect(); 933 - let prefix = 934 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 935 - (path, prefix) 936 - }; 937 - 938 - // Find the variable ID 939 - let var_id = match var_name { 940 - "env" => Some(ENV_VARIABLE_ID), 941 - "nu" => Some(NU_VARIABLE_ID), 942 - "in" => Some(IN_VARIABLE_ID), 943 - _ => { 944 - // Try to find user-defined variable 945 - working_set.find_variable(var_name.as_bytes()) 946 - } 947 - }; 948 - 949 - if let Some(var_id) = var_id { 950 - // Calculate span for the cell path member being completed 951 - let prefix_byte_len = cell_prefix.len(); 952 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 953 - context = Some(CompletionContext::CellPath { 954 - prefix: cell_prefix, 955 - span: Span::new(cell_span_start, span.end), 956 - var_id, 957 - path_so_far, 958 - }); 959 - } else { 960 - // Unknown variable, fall back to variable completion 961 - let var_prefix = trimmed_prefix[1..].to_string(); 962 - context = Some(CompletionContext::Variable { 963 - prefix: var_prefix, 964 - span, 965 - }); 966 - } 967 - } else { 968 - // Variable completion context (no dot) 969 - let var_prefix = if trimmed_prefix.len() > 1 { 970 - trimmed_prefix[1..].to_string() 971 - } else { 972 - String::new() 973 - }; 974 - context = Some(CompletionContext::Variable { 975 - prefix: var_prefix, 976 - span, 977 - }); 978 - } 979 - } 980 - _ if is_command_shape(shape, local_span) => { 981 - let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix); 982 - context = Some(CompletionContext::Command { 983 - prefix: full_prefix, 984 - span: full_span, 985 - }); 986 - } 987 - FlatShape::Block | FlatShape::Closure => { 988 - if let Some(ctx) = handle_block_or_closure( 989 - &prefix, 990 - span, 991 - shape.as_str().trim_start_matches("shape_"), 992 - idx, 993 - local_span, 994 - ) { 995 - context = Some(ctx); 996 - } 997 - } 998 - FlatShape::Variable(var_id) => { 999 - // Variable or cell path completion context 1000 - let trimmed_prefix = prefix.trim(); 1001 - if trimmed_prefix.starts_with('$') { 1002 - // Check if this is a cell path (contains a dot after $) 1003 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 1004 - // Cell path completion 1005 - let after_var = &trimmed_prefix[dot_pos + 2..]; 1006 - let parts: Vec<&str> = after_var.split('.').collect(); 1007 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1008 - (vec![], String::new()) 1009 - } else if after_var.ends_with('.') { 1010 - ( 1011 - parts 1012 - .iter() 1013 - .filter(|s| !s.is_empty()) 1014 - .map(|s| s.to_string()) 1015 - .collect(), 1016 - String::new(), 1017 - ) 1018 - } else { 1019 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1020 - .iter() 1021 - .map(|s| s.to_string()) 1022 - .collect(); 1023 - let prefix = 1024 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 1025 - (path, prefix) 1026 - }; 1027 - 1028 - let prefix_byte_len = cell_prefix.len(); 1029 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 1030 - context = Some(CompletionContext::CellPath { 1031 - prefix: cell_prefix, 1032 - span: Span::new(cell_span_start, span.end), 1033 - var_id: *var_id, 1034 - path_so_far, 1035 - }); 1036 - } else { 1037 - // Simple variable completion 1038 - let var_prefix = trimmed_prefix[1..].to_string(); 1039 - context = Some(CompletionContext::Variable { 1040 - prefix: var_prefix, 1041 - span, 1042 - }); 1043 - } 1044 - } else { 1045 - // Fallback to argument context if no $ found 1046 - context = Some(CompletionContext::Argument { prefix, span }); 1047 - } 1048 - } 1049 - _ => { 1050 - // Check if this is a variable or cell path (starts with $) 1051 - let trimmed_prefix = prefix.trim(); 1052 - if trimmed_prefix.starts_with('$') { 1053 - // Check if this is a cell path (contains a dot after $) 1054 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 1055 - // Cell path completion 1056 - let var_name = &trimmed_prefix[1..dot_pos + 1]; 1057 - let after_var = &trimmed_prefix[dot_pos + 2..]; 1058 - let parts: Vec<&str> = after_var.split('.').collect(); 1059 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1060 - (vec![], String::new()) 1061 - } else if after_var.ends_with('.') { 1062 - ( 1063 - parts 1064 - .iter() 1065 - .filter(|s| !s.is_empty()) 1066 - .map(|s| s.to_string()) 1067 - .collect(), 1068 - String::new(), 1069 - ) 1070 - } else { 1071 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1072 - .iter() 1073 - .map(|s| s.to_string()) 1074 - .collect(); 1075 - let prefix = 1076 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 1077 - (path, prefix) 1078 - }; 1079 - 1080 - let var_id = match var_name { 1081 - "env" => Some(ENV_VARIABLE_ID), 1082 - "nu" => Some(NU_VARIABLE_ID), 1083 - "in" => Some(IN_VARIABLE_ID), 1084 - _ => working_set.find_variable(var_name.as_bytes()), 1085 - }; 1086 - 1087 - if let Some(var_id) = var_id { 1088 - let prefix_byte_len = cell_prefix.len(); 1089 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 1090 - context = Some(CompletionContext::CellPath { 1091 - prefix: cell_prefix, 1092 - span: Span::new(cell_span_start, span.end), 1093 - var_id, 1094 - path_so_far, 1095 - }); 1096 - } else { 1097 - let var_prefix = trimmed_prefix[1..].to_string(); 1098 - context = Some(CompletionContext::Variable { 1099 - prefix: var_prefix, 1100 - span, 1101 - }); 1102 - } 1103 - } else { 1104 - // Simple variable completion 1105 - let var_prefix = if trimmed_prefix.len() > 1 { 1106 - trimmed_prefix[1..].to_string() 1107 - } else { 1108 - String::new() 1109 - }; 1110 - context = Some(CompletionContext::Variable { 1111 - prefix: var_prefix, 1112 - span, 1113 - }); 1114 - } 1115 - } else if trimmed_prefix.starts_with('-') { 1116 - // This looks like a flag - find the command 1117 - if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span) 1118 - { 1119 - context = Some(CompletionContext::Flag { 1120 - prefix: trimmed_prefix.to_string(), 1121 - span, 1122 - command_name: cmd_name, 1123 - }); 1124 - } else { 1125 - context = Some(CompletionContext::Argument { prefix, span }); 1126 - } 1127 - } else { 1128 - // This is a positional argument - find the command and argument index 1129 - if let Some((cmd_name, arg_index)) = 1130 - find_command_and_arg_index(idx, local_span) 1131 - { 1132 - context = Some(CompletionContext::CommandArgument { 1133 - prefix: trimmed_prefix.to_string(), 1134 - span, 1135 - command_name: cmd_name, 1136 - arg_index, 1137 - }); 1138 - } else { 1139 - context = Some(CompletionContext::Argument { prefix, span }); 1140 - } 1141 - } 1142 - } 1143 - } 1144 - } 1145 - break; 1146 - } 1147 - } 1148 - 1149 - // If not in a shape, check what comes before the cursor 1150 - if context.is_none() { 1151 - web_sys::console::log_1(&JsValue::from_str( 1152 - "[completion] Context is None, entering fallback logic", 1153 - )); 1154 - // Check if there's a command-like shape before us 1155 - let mut found_command_before = false; 1156 - let mut has_separator_after_command = false; 1157 - for (span, shape) in shapes.iter().rev() { 1158 - let local_span = to_local_span(*span); 1159 - if local_span.end <= byte_pos { 1160 - if is_command_shape(shape, local_span) { 1161 - // Check if there's a pipe or semicolon between this command and the cursor 1162 - has_separator_after_command = has_separator_between(local_span.end, byte_pos); 1163 - web_sys::console::log_1(&JsValue::from_str(&format!( 1164 - "[completion] Found command shape {:?} at {:?}, has_separator_after_command={}", 1165 - shape, local_span, has_separator_after_command 1166 - ))); 1167 - if !has_separator_after_command { 1168 - found_command_before = true; 1169 - 1170 - // Extract the command text 1171 - let cmd = safe_slice(local_span); 1172 - let cmd_name = cmd.split_whitespace().next().unwrap_or(&cmd).trim(); 1173 - 1174 - // Check if we're right after the command (only whitespace between command and cursor) 1175 - let text_after_command = if local_span.end < input.len() { 1176 - &input[local_span.end..byte_pos] 1177 - } else { 1178 - "" 1179 - }; 1180 - let is_right_after_command = text_after_command.trim().is_empty(); 1181 - 1182 - // If we're right after a command, check if it has positional arguments 1183 - if is_right_after_command { 1184 - if let Some(signature) = get_command_signature(cmd_name) { 1185 - // Check if command has any positional arguments 1186 - let has_positional_args = !signature.required_positional.is_empty() 1187 - || !signature.optional_positional.is_empty(); 1188 - 1189 - if has_positional_args { 1190 - // Count existing arguments before cursor 1191 - let mut arg_count = 0; 1192 - for (prev_span, prev_shape) in shapes.iter().rev() { 1193 - let prev_local_span = to_local_span(*prev_span); 1194 - if prev_local_span.end <= byte_pos 1195 - && prev_local_span.end > local_span.end 1196 - { 1197 - if !is_command_shape(prev_shape, prev_local_span) { 1198 - let arg_text = safe_slice(prev_local_span); 1199 - let trimmed_arg = arg_text.trim(); 1200 - // Don't count flags (starting with -) or empty arguments 1201 - if !trimmed_arg.is_empty() 1202 - && !trimmed_arg.starts_with('-') 1203 - { 1204 - arg_count += 1; 1205 - } 1206 - } 1207 - } 1208 - } 1209 - 1210 - web_sys::console::log_1(&JsValue::from_str(&format!( 1211 - "[completion] Right after command {:?}, setting CommandArgument context with arg_index: {}", 1212 - cmd_name, arg_count 1213 - ))); 1214 - 1215 - context = Some(CompletionContext::CommandArgument { 1216 - prefix: String::new(), 1217 - span: Span::new(byte_pos, byte_pos), 1218 - command_name: cmd_name.to_string(), 1219 - arg_index: arg_count, 1220 - }); 1221 - } else { 1222 - // No positional arguments, don't show any completions 1223 - web_sys::console::log_1(&JsValue::from_str(&format!( 1224 - "[completion] Command {:?} has no positional args, not showing completions", 1225 - cmd_name 1226 - ))); 1227 - // Leave context as None to show no completions 1228 - } 1229 - } else { 1230 - // Couldn't find signature, don't show completions 1231 - web_sys::console::log_1(&JsValue::from_str(&format!( 1232 - "[completion] Could not find signature for {:?}, not showing completions", 1233 - cmd_name 1234 - ))); 1235 - // Leave context as None to show no completions 1236 - } 1237 - } else { 1238 - // Not right after command, complete the command itself 1239 - web_sys::console::log_1(&JsValue::from_str(&format!( 1240 - "[completion] Set Command context with prefix: {:?}", 1241 - cmd 1242 - ))); 1243 - context = Some(CompletionContext::Command { 1244 - prefix: cmd, 1245 - span: local_span, 1246 - }); 1247 - } 1248 - } 1249 - } 1250 - break; 1251 - } 1252 - } 1253 - 1254 - if !found_command_before { 1255 - web_sys::console::log_1(&JsValue::from_str( 1256 - "[completion] No command found before cursor, checking tokens", 1257 - )); 1258 - // No command before, check context from tokens 1259 - let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true); 1260 - let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 1261 - 1262 - let is_cmd_context = if let Some(token) = last_token { 1263 - let matches = matches!( 1264 - token.contents, 1265 - TokenContents::Pipe 1266 - | TokenContents::PipePipe 1267 - | TokenContents::Semicolon 1268 - | TokenContents::Eol 1269 - ); 1270 - web_sys::console::log_1(&JsValue::from_str(&format!( 1271 - "[completion] Last token: {:?}, is_cmd_context from token={}", 1272 - token.contents, matches 1273 - ))); 1274 - matches 1275 - } else { 1276 - web_sys::console::log_1(&JsValue::from_str( 1277 - "[completion] No last token found, assuming start of input (is_cmd_context=true)", 1278 - )); 1279 - true // Start of input 1280 - }; 1281 - 1282 - // Look for the last non-whitespace token before cursor 1283 - let text_before = &input[..byte_pos]; 1284 - 1285 - // Also check if we're inside a block - if the last non-whitespace char before cursor is '{' 1286 - let text_before_trimmed = text_before.trim_end(); 1287 - let is_inside_block = text_before_trimmed.ends_with('{'); 1288 - // If we found a separator after a command, we're starting a new command 1289 - let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command; 1290 - web_sys::console::log_1(&JsValue::from_str(&format!( 1291 - "[completion] is_inside_block={}, has_separator_after_command={}, final is_cmd_context={}", 1292 - is_inside_block, has_separator_after_command, is_cmd_context 1293 - ))); 1294 - 1295 - // Find the last word before cursor 1296 - let last_word_start = text_before 1297 - .rfind(|c: char| c.is_whitespace() || is_separator_char(c)) 1298 - .map(|i| i + 1) 1299 - .unwrap_or(0); 1300 - 1301 - let last_word = text_before[last_word_start..].trim_start(); 1302 - web_sys::console::log_1(&JsValue::from_str(&format!( 1303 - "[completion] last_word_start={}, last_word={:?}", 1304 - last_word_start, last_word 1305 - ))); 1306 - 1307 - if is_cmd_context { 1308 - context = Some(CompletionContext::Command { 1309 - prefix: last_word.to_string(), 1310 - span: Span::new(last_word_start, byte_pos), 1311 - }); 1312 - web_sys::console::log_1(&JsValue::from_str(&format!( 1313 - "[completion] Set Command context with prefix: {:?}", 1314 - last_word 1315 - ))); 1316 - } else { 1317 - // Check if this is a variable or cell path (starts with $) 1318 - let trimmed_word = last_word.trim(); 1319 - if trimmed_word.starts_with('$') { 1320 - // Check if this is a cell path (contains a dot after $) 1321 - if let Some(dot_pos) = trimmed_word[1..].find('.') { 1322 - // Cell path completion 1323 - let var_name = &trimmed_word[1..dot_pos + 1]; 1324 - let after_var = &trimmed_word[dot_pos + 2..]; 1325 - let parts: Vec<&str> = after_var.split('.').collect(); 1326 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1327 - (vec![], String::new()) 1328 - } else if after_var.ends_with('.') { 1329 - ( 1330 - parts 1331 - .iter() 1332 - .filter(|s| !s.is_empty()) 1333 - .map(|s| s.to_string()) 1334 - .collect(), 1335 - String::new(), 1336 - ) 1337 - } else { 1338 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1339 - .iter() 1340 - .map(|s| s.to_string()) 1341 - .collect(); 1342 - let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 1343 - (path, prefix) 1344 - }; 1345 - 1346 - let var_id = match var_name { 1347 - "env" => Some(ENV_VARIABLE_ID), 1348 - "nu" => Some(NU_VARIABLE_ID), 1349 - "in" => Some(IN_VARIABLE_ID), 1350 - _ => working_set.find_variable(var_name.as_bytes()), 1351 - }; 1352 - 1353 - if let Some(var_id) = var_id { 1354 - let prefix_byte_len = cell_prefix.len(); 1355 - let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 1356 - let cell_prefix_clone = cell_prefix.clone(); 1357 - context = Some(CompletionContext::CellPath { 1358 - prefix: cell_prefix, 1359 - span: Span::new(cell_span_start, byte_pos), 1360 - var_id, 1361 - path_so_far, 1362 - }); 1363 - web_sys::console::log_1(&JsValue::from_str(&format!( 1364 - "[completion] Set CellPath context with prefix: {:?}", 1365 - cell_prefix_clone 1366 - ))); 1367 - } else { 1368 - let var_prefix = trimmed_word[1..].to_string(); 1369 - let var_prefix_clone = var_prefix.clone(); 1370 - context = Some(CompletionContext::Variable { 1371 - prefix: var_prefix, 1372 - span: Span::new(last_word_start, byte_pos), 1373 - }); 1374 - web_sys::console::log_1(&JsValue::from_str(&format!( 1375 - "[completion] Set Variable context with prefix: {:?}", 1376 - var_prefix_clone 1377 - ))); 1378 - } 1379 - } else { 1380 - // Simple variable completion 1381 - let var_prefix = trimmed_word[1..].to_string(); 1382 - let var_prefix_clone = var_prefix.clone(); 1383 - context = Some(CompletionContext::Variable { 1384 - prefix: var_prefix, 1385 - span: Span::new(last_word_start, byte_pos), 1386 - }); 1387 - web_sys::console::log_1(&JsValue::from_str(&format!( 1388 - "[completion] Set Variable context with prefix: {:?}", 1389 - var_prefix_clone 1390 - ))); 1391 - } 1392 - } else if trimmed_word.starts_with('-') { 1393 - // Try to find command by looking backwards through shapes 1394 - let mut found_cmd = None; 1395 - for (span, shape) in shapes.iter().rev() { 1396 - let local_span = to_local_span(*span); 1397 - if local_span.end <= byte_pos && is_command_shape(shape, local_span) { 1398 - let cmd_text = safe_slice(local_span); 1399 - let cmd_name = cmd_text 1400 - .split_whitespace() 1401 - .next() 1402 - .unwrap_or(&cmd_text) 1403 - .trim(); 1404 - found_cmd = Some(cmd_name.to_string()); 1405 - break; 1406 - } 1407 - } 1408 - if let Some(cmd_name) = found_cmd { 1409 - let cmd_name_clone = cmd_name.clone(); 1410 - context = Some(CompletionContext::Flag { 1411 - prefix: trimmed_word.to_string(), 1412 - span: Span::new(last_word_start, byte_pos), 1413 - command_name: cmd_name, 1414 - }); 1415 - web_sys::console::log_1(&JsValue::from_str(&format!( 1416 - "[completion] Set Flag context with prefix: {:?}, command: {:?}", 1417 - trimmed_word, cmd_name_clone 1418 - ))); 1419 - } else { 1420 - context = Some(CompletionContext::Argument { 1421 - prefix: last_word.to_string(), 1422 - span: Span::new(last_word_start, byte_pos), 1423 - }); 1424 - web_sys::console::log_1(&JsValue::from_str(&format!( 1425 - "[completion] Set Argument context with prefix: {:?}", 1426 - last_word 1427 - ))); 1428 - } 1429 - } else { 1430 - // Try to find command and argument index 1431 - let mut found_cmd = None; 1432 - let mut arg_count = 0; 1433 - for (span, shape) in shapes.iter().rev() { 1434 - let local_span = to_local_span(*span); 1435 - if local_span.end <= byte_pos { 1436 - if is_command_shape(shape, local_span) { 1437 - let cmd_text = safe_slice(local_span); 1438 - let cmd_name = cmd_text 1439 - .split_whitespace() 1440 - .next() 1441 - .unwrap_or(&cmd_text) 1442 - .trim(); 1443 - found_cmd = Some(cmd_name.to_string()); 1444 - break; 1445 - } else { 1446 - let arg_text = safe_slice(local_span); 1447 - let trimmed_arg = arg_text.trim(); 1448 - if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 1449 - arg_count += 1; 1450 - } 1451 - } 1452 - } 1453 - } 1454 - if let Some(cmd_name) = found_cmd { 1455 - let cmd_name_clone = cmd_name.clone(); 1456 - context = Some(CompletionContext::CommandArgument { 1457 - prefix: trimmed_word.to_string(), 1458 - span: Span::new(last_word_start, byte_pos), 1459 - command_name: cmd_name, 1460 - arg_index: arg_count, 1461 - }); 1462 - web_sys::console::log_1(&JsValue::from_str(&format!( 1463 - "[completion] Set CommandArgument context with prefix: {:?}, command: {:?}, arg_index: {}", 1464 - trimmed_word, cmd_name_clone, arg_count 1465 - ))); 1466 - } else { 1467 - context = Some(CompletionContext::Argument { 1468 - prefix: last_word.to_string(), 1469 - span: Span::new(last_word_start, byte_pos), 1470 - }); 1471 - web_sys::console::log_1(&JsValue::from_str(&format!( 1472 - "[completion] Set Argument context with prefix: {:?}", 1473 - last_word 1474 - ))); 1475 - } 1476 - } 1477 - } 1478 - } 1479 - } 1480 - 1481 - web_sys::console::log_1(&JsValue::from_str(&format!("context: {:?}", context))); 1482 - 1483 - let mut suggestions: Vec<Suggestion> = Vec::new(); 1484 - 1485 - // Convert byte-spans back to char-spans for JS 1486 - let to_char_span = |span: Span| -> Span { 1487 - let char_start = input[..span.start].chars().count(); 1488 - let char_end = input[..span.end].chars().count(); 1489 - Span::new(char_start, char_end) 1490 - }; 1491 - 1492 - match context { 1493 - Some(CompletionContext::Command { prefix, span }) => { 1494 - web_sys::console::log_1(&JsValue::from_str(&format!( 1495 - "[completion] Generating Command suggestions with prefix: {:?}", 1496 - prefix 1497 - ))); 1498 - // Command completion 1499 - let cmds = working_set 1500 - .find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); 1501 - 1502 - let span = to_char_span(span); 1503 - let mut cmd_count = 0; 1504 - 1505 - for (_, name, desc, _) in cmds { 1506 - let name_str = String::from_utf8_lossy(&name).to_string(); 1507 - suggestions.push(Suggestion { 1508 - rendered: { 1509 - let name_colored = ansi_term::Color::Green.bold().paint(&name_str); 1510 - let desc_str = desc.as_deref().unwrap_or("<no description>"); 1511 - format!("{name_colored} {desc_str}") 1512 - }, 1513 - name: name_str, 1514 - description: desc, 1515 - is_command: true, 1516 - span_start: span.start, 1517 - span_end: span.end, 1518 - }); 1519 - cmd_count += 1; 1520 - } 1521 - web_sys::console::log_1(&JsValue::from_str(&format!( 1522 - "[completion] Found {} command suggestions", 1523 - cmd_count 1524 - ))); 1525 - } 1526 - Some(CompletionContext::Argument { prefix, span }) => { 1527 - web_sys::console::log_1(&JsValue::from_str(&format!( 1528 - "[completion] Generating Argument suggestions with prefix: {:?}", 1529 - prefix 1530 - ))); 1531 - // File completion 1532 - let (dir, file_prefix) = prefix 1533 - .rfind('/') 1534 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1535 - .unwrap_or(("", prefix.as_str())); 1536 - 1537 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1538 - .then(|| &dir[..dir.len() - 1]) 1539 - .unwrap_or(dir); 1540 - 1541 - let target_dir = if !dir.is_empty() { 1542 - match root.join(dir_to_join) { 1543 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1544 - _ => None, 1545 - } 1546 - } else { 1547 - Some(root.join("").unwrap()) 1548 - }; 1549 - 1550 - let mut file_count = 0; 1551 - if let Some(d) = target_dir { 1552 - if let Ok(iterator) = d.read_dir() { 1553 - let span = to_char_span(span); 1554 - 1555 - for entry in iterator { 1556 - let name = entry.filename(); 1557 - if name.starts_with(file_prefix) { 1558 - let full_completion = format!("{}{}", dir, name); 1559 - suggestions.push(Suggestion { 1560 - name: full_completion.clone(), 1561 - description: None, 1562 - is_command: false, 1563 - rendered: full_completion, 1564 - span_start: span.start, 1565 - span_end: span.end, 1566 - }); 1567 - file_count += 1; 1568 - } 1569 - } 1570 - } 1571 - } 1572 - web_sys::console::log_1(&JsValue::from_str(&format!( 1573 - "[completion] Found {} file suggestions", 1574 - file_count 1575 - ))); 1576 - } 1577 - Some(CompletionContext::Flag { 1578 - prefix, 1579 - span, 1580 - command_name, 1581 - }) => { 1582 - web_sys::console::log_1(&JsValue::from_str(&format!( 1583 - "[completion] Generating Flag suggestions for command: {:?}, prefix: {:?}", 1584 - command_name, prefix 1585 - ))); 1586 - 1587 - if let Some(signature) = get_command_signature(&command_name) { 1588 - let span = to_char_span(span); 1589 - let mut flag_count = 0; 1590 - 1591 - // Get switches from signature 1592 - // Signature has a named field that contains named arguments (including switches) 1593 - for flag in &signature.named { 1594 - // Check if this is a switch (has no argument) 1595 - // Switches have arg: None, named arguments have arg: Some(SyntaxShape) 1596 - let is_switch = flag.arg.is_none(); 1597 - 1598 - if is_switch { 1599 - let long_name = format!("--{}", flag.long); 1600 - let short_name = flag.short.map(|c| format!("-{}", c)); 1601 - 1602 - // Determine which flags to show based on prefix: 1603 - // - If prefix is empty or exactly "-", show all flags (both short and long) 1604 - // - If prefix starts with "--", only show long flags that match the prefix 1605 - // - If prefix starts with "-" (but not "--"), only show short flags that match the prefix 1606 - let show_all = prefix.is_empty() || prefix == "-"; 1607 - 1608 - // Helper to create a flag suggestion 1609 - let create_flag_suggestion = |flag_name: String| -> Suggestion { 1610 - Suggestion { 1611 - name: flag_name.clone(), 1612 - description: Some(flag.desc.clone()), 1613 - is_command: false, 1614 - rendered: { 1615 - let flag_colored = 1616 - ansi_term::Color::Cyan.bold().paint(&flag_name); 1617 - format!("{flag_colored} {}", flag.desc) 1618 - }, 1619 - span_start: span.start, 1620 - span_end: span.end, 1621 - } 1622 - }; 1623 - 1624 - // Add long flag if it matches 1625 - let should_show_long = if show_all { 1626 - true // Show all flags when prefix is "-" or empty 1627 - } else if prefix.starts_with("--") { 1628 - long_name.starts_with(&prefix) // Only show long flags matching prefix 1629 - } else { 1630 - false // Don't show long flags if prefix is short flag format 1631 - }; 1632 - 1633 - if should_show_long { 1634 - suggestions.push(create_flag_suggestion(long_name)); 1635 - flag_count += 1; 1636 - } 1637 - 1638 - // Add short flag if it matches 1639 - if let Some(short) = &short_name { 1640 - let should_show_short = if show_all { 1641 - true // Show all flags when prefix is "-" or empty 1642 - } else if prefix.starts_with("-") && !prefix.starts_with("--") { 1643 - short.starts_with(&prefix) // Only show short flags matching prefix 1644 - } else { 1645 - false // Don't show short flags if prefix is long flag format 1646 - }; 1647 - 1648 - if should_show_short { 1649 - suggestions.push(create_flag_suggestion(short.clone())); 1650 - flag_count += 1; 1651 - } 1652 - } 1653 - } 1654 - } 1655 - 1656 - web_sys::console::log_1(&JsValue::from_str(&format!( 1657 - "[completion] Found {} flag suggestions", 1658 - flag_count 1659 - ))); 1660 - } else { 1661 - web_sys::console::log_1(&JsValue::from_str(&format!( 1662 - "[completion] Could not find signature for command: {:?}", 1663 - command_name 1664 - ))); 1665 - } 1666 - } 1667 - Some(CompletionContext::CommandArgument { 1668 - prefix, 1669 - span, 1670 - command_name, 1671 - arg_index, 1672 - }) => { 1673 - web_sys::console::log_1(&JsValue::from_str(&format!( 1674 - "[completion] Generating CommandArgument suggestions for command: {:?}, arg_index: {}, prefix: {:?}", 1675 - command_name, arg_index, prefix 1676 - ))); 1677 - 1678 - if let Some(signature) = get_command_signature(&command_name) { 1679 - // Get positional arguments from signature 1680 - // Combine required and optional positional arguments 1681 - let mut all_positional = Vec::new(); 1682 - all_positional.extend_from_slice(&signature.required_positional); 1683 - all_positional.extend_from_slice(&signature.optional_positional); 1684 - 1685 - // Find the argument at the given index 1686 - if let Some(arg) = all_positional.get(arg_index) { 1687 - // Check the SyntaxShape to determine completion type 1688 - match &arg.shape { 1689 - nu_protocol::SyntaxShape::String | nu_protocol::SyntaxShape::Filepath => { 1690 - // File/directory completion 1691 - let (dir, file_prefix) = prefix 1692 - .rfind('/') 1693 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1694 - .unwrap_or(("", prefix.as_str())); 1695 - 1696 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1697 - .then(|| &dir[..dir.len() - 1]) 1698 - .unwrap_or(dir); 1699 - 1700 - let target_dir = if !dir.is_empty() { 1701 - match root.join(dir_to_join) { 1702 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1703 - _ => None, 1704 - } 1705 - } else { 1706 - Some(root.join("").unwrap()) 1707 - }; 1708 - 1709 - let span = to_char_span(span); 1710 - let mut file_count = 0; 1711 - if let Some(d) = target_dir { 1712 - if let Ok(iterator) = d.read_dir() { 1713 - for entry in iterator { 1714 - let name = entry.filename(); 1715 - if name.starts_with(file_prefix) { 1716 - let full_completion = format!("{}{}", dir, name); 1717 - suggestions.push(Suggestion { 1718 - name: full_completion.clone(), 1719 - description: Some(arg.desc.clone()), 1720 - is_command: false, 1721 - rendered: full_completion, 1722 - span_start: span.start, 1723 - span_end: span.end, 1724 - }); 1725 - file_count += 1; 1726 - } 1727 - } 1728 - } 1729 - } 1730 - web_sys::console::log_1(&JsValue::from_str(&format!( 1731 - "[completion] Found {} file suggestions for argument {}", 1732 - file_count, arg_index 1733 - ))); 1734 - } 1735 - _ => { 1736 - // For other types, fall back to file completion 1737 - let (dir, file_prefix) = prefix 1738 - .rfind('/') 1739 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1740 - .unwrap_or(("", prefix.as_str())); 1741 - 1742 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1743 - .then(|| &dir[..dir.len() - 1]) 1744 - .unwrap_or(dir); 1745 - 1746 - let target_dir = if !dir.is_empty() { 1747 - match root.join(dir_to_join) { 1748 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1749 - _ => None, 1750 - } 1751 - } else { 1752 - Some(root.join("").unwrap()) 1753 - }; 1754 - 1755 - let span = to_char_span(span); 1756 - if let Some(d) = target_dir { 1757 - if let Ok(iterator) = d.read_dir() { 1758 - for entry in iterator { 1759 - let name = entry.filename(); 1760 - if name.starts_with(file_prefix) { 1761 - let full_completion = format!("{}{}", dir, name); 1762 - suggestions.push(Suggestion { 1763 - name: full_completion.clone(), 1764 - description: Some(arg.desc.clone()), 1765 - is_command: false, 1766 - rendered: full_completion, 1767 - span_start: span.start, 1768 - span_end: span.end, 1769 - }); 1770 - } 1771 - } 1772 - } 1773 - } 1774 - } 1775 - } 1776 - } else { 1777 - // Argument index out of range, fall back to file completion 1778 - web_sys::console::log_1(&JsValue::from_str(&format!( 1779 - "[completion] Argument index {} out of range, using file completion", 1780 - arg_index 1781 - ))); 1782 - // Use the same file completion logic as Argument context 1783 - let (dir, file_prefix) = prefix 1784 - .rfind('/') 1785 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1786 - .unwrap_or(("", prefix.as_str())); 1787 - 1788 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1789 - .then(|| &dir[..dir.len() - 1]) 1790 - .unwrap_or(dir); 1791 - 1792 - let target_dir = if !dir.is_empty() { 1793 - match root.join(dir_to_join) { 1794 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1795 - _ => None, 1796 - } 1797 - } else { 1798 - Some(root.join("").unwrap()) 1799 - }; 1800 - 1801 - let span = to_char_span(span); 1802 - if let Some(d) = target_dir { 1803 - if let Ok(iterator) = d.read_dir() { 1804 - for entry in iterator { 1805 - let name = entry.filename(); 1806 - if name.starts_with(file_prefix) { 1807 - let full_completion = format!("{}{}", dir, name); 1808 - suggestions.push(Suggestion { 1809 - name: full_completion.clone(), 1810 - description: None, 1811 - is_command: false, 1812 - rendered: full_completion, 1813 - span_start: span.start, 1814 - span_end: span.end, 1815 - }); 1816 - } 1817 - } 1818 - } 1819 - } 1820 - } 1821 - } else { 1822 - // No signature found, fall back to file completion 1823 - web_sys::console::log_1(&JsValue::from_str(&format!( 1824 - "[completion] Could not find signature for command: {:?}, using file completion", 1825 - command_name 1826 - ))); 1827 - let (dir, file_prefix) = prefix 1828 - .rfind('/') 1829 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1830 - .unwrap_or(("", prefix.as_str())); 1831 - 1832 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1833 - .then(|| &dir[..dir.len() - 1]) 1834 - .unwrap_or(dir); 1835 - 1836 - let target_dir = if !dir.is_empty() { 1837 - match root.join(dir_to_join) { 1838 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1839 - _ => None, 1840 - } 1841 - } else { 1842 - Some(root.join("").unwrap()) 1843 - }; 1844 - 1845 - let span = to_char_span(span); 1846 - if let Some(d) = target_dir { 1847 - if let Ok(iterator) = d.read_dir() { 1848 - for entry in iterator { 1849 - let name = entry.filename(); 1850 - if name.starts_with(file_prefix) { 1851 - let full_completion = format!("{}{}", dir, name); 1852 - suggestions.push(Suggestion { 1853 - name: full_completion.clone(), 1854 - description: None, 1855 - is_command: false, 1856 - rendered: full_completion, 1857 - span_start: span.start, 1858 - span_end: span.end, 1859 - }); 1860 - } 1861 - } 1862 - } 1863 - } 1864 - } 1865 - } 1866 - Some(CompletionContext::Variable { prefix, span }) => { 1867 - web_sys::console::log_1(&JsValue::from_str(&format!( 1868 - "[completion] Generating Variable suggestions with prefix: {:?}", 1869 - prefix 1870 - ))); 1871 - 1872 - // Collect all available variables 1873 - let variables = collect_variables(&working_set, &input, byte_pos); 1874 - let span = to_char_span(span); 1875 - let mut var_count = 0; 1876 - 1877 - for (var_name, var_id) in variables { 1878 - // Filter by prefix (variable name includes $, so we need to check after $) 1879 - if var_name.len() > 1 && var_name[1..].starts_with(&prefix) { 1880 - // Get variable type 1881 - let var_type = working_set.get_variable(var_id).ty.to_string(); 1882 - 1883 - suggestions.push(Suggestion { 1884 - name: var_name.clone(), 1885 - description: Some(var_type.clone()), 1886 - is_command: false, 1887 - rendered: { 1888 - let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); 1889 - format!("{var_colored} {var_type}") 1890 - }, 1891 - span_start: span.start, 1892 - span_end: span.end, 1893 - }); 1894 - var_count += 1; 1895 - } 1896 - } 1897 - 1898 - web_sys::console::log_1(&JsValue::from_str(&format!( 1899 - "[completion] Found {} variable suggestions", 1900 - var_count 1901 - ))); 1902 - } 1903 - Some(CompletionContext::CellPath { 1904 - prefix, 1905 - span, 1906 - var_id, 1907 - path_so_far, 1908 - }) => { 1909 - web_sys::console::log_1(&JsValue::from_str(&format!( 1910 - "[completion] Generating CellPath suggestions with prefix: {:?}, path: {:?}", 1911 - prefix, path_so_far 1912 - ))); 1913 - 1914 - // Evaluate the variable to get its value 1915 - if let Some(var_value) = eval_variable_for_completion(var_id, &working_set) { 1916 - // Follow the path to get the value at the current level 1917 - let current_value = if path_so_far.is_empty() { 1918 - var_value 1919 - } else { 1920 - follow_cell_path(&var_value, &path_so_far).unwrap_or(var_value) 1921 - }; 1922 - 1923 - // Get columns/fields from the current value 1924 - let columns = get_columns_from_value(&current_value); 1925 - let span = to_char_span(span); 1926 - let mut field_count = 0; 1927 - 1928 - for (col_name, col_type) in columns { 1929 - // Filter by prefix 1930 - if col_name.starts_with(&prefix) { 1931 - let type_str = col_type.as_deref().unwrap_or("any"); 1932 - suggestions.push(Suggestion { 1933 - name: col_name.clone(), 1934 - description: Some(type_str.to_string()), 1935 - is_command: false, 1936 - rendered: { 1937 - let col_colored = ansi_term::Color::Yellow.paint(&col_name); 1938 - format!("{col_colored} {type_str}") 1939 - }, 1940 - span_start: span.start, 1941 - span_end: span.end, 1942 - }); 1943 - field_count += 1; 1944 - } 1945 - } 1946 - 1947 - web_sys::console::log_1(&JsValue::from_str(&format!( 1948 - "[completion] Found {} cell path suggestions", 1949 - field_count 1950 - ))); 1951 - } else { 1952 - // Variable couldn't be evaluated - this is expected for runtime variables 1953 - // We can't provide cell path completions without knowing the structure 1954 - web_sys::console::log_1(&JsValue::from_str(&format!( 1955 - "[completion] Could not evaluate variable {:?} for cell path completion (runtime variable)", 1956 - var_id 1957 - ))); 1958 - 1959 - // Try to get type information to provide better feedback 1960 - if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 1961 - working_set.get_variable(var_id) 1962 - })) { 1963 - web_sys::console::log_1(&JsValue::from_str(&format!( 1964 - "[completion] Variable type: {:?}", 1965 - var_info.ty 1966 - ))); 1967 - } 1968 - } 1969 - } 1970 - _ => { 1971 - web_sys::console::log_1(&JsValue::from_str( 1972 - "[completion] Context is None, no suggestions generated", 1973 - )); 1974 - } 1975 - } 1976 - 1977 - drop(working_set); 1978 - drop(engine_guard); 1979 - 1980 - suggestions.sort(); 1981 - let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()); 1982 - web_sys::console::log_1(&JsValue::from_str(&suggestions)); 1983 - suggestions 1984 - }
+949
src/completion/context.rs
··· 1 + use crate::completion::helpers::*; 2 + use crate::completion::types::CompletionContext; 3 + use crate::console_log; 4 + use nu_parser::FlatShape; 5 + use nu_protocol::engine::{EngineState, StateWorkingSet}; 6 + use nu_protocol::{Signature, Span}; 7 + 8 + pub 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 + 55 + pub 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 + 104 + pub 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 + pub fn determine_flag_or_argument_context( 111 + input: &str, 112 + shapes: &[(Span, FlatShape)], 113 + prefix: &str, 114 + idx: usize, 115 + local_span: Span, 116 + span: Span, 117 + global_offset: usize, 118 + ) -> CompletionContext { 119 + let trimmed_prefix = prefix.trim(); 120 + if trimmed_prefix.starts_with('-') { 121 + // This looks like a flag - find the command 122 + if let Some((cmd_name, _)) = 123 + find_command_and_arg_index(input, shapes, idx, local_span, global_offset) 124 + { 125 + CompletionContext::Flag { 126 + prefix: trimmed_prefix.to_string(), 127 + span, 128 + command_name: cmd_name, 129 + } 130 + } else { 131 + CompletionContext::Argument { 132 + prefix: prefix.to_string(), 133 + span, 134 + } 135 + } 136 + } else { 137 + // This is a positional argument - find the command and argument index 138 + if let Some((cmd_name, arg_index)) = 139 + find_command_and_arg_index(input, shapes, idx, local_span, global_offset) 140 + { 141 + CompletionContext::CommandArgument { 142 + prefix: trimmed_prefix.to_string(), 143 + span, 144 + command_name: cmd_name, 145 + arg_index, 146 + } 147 + } else { 148 + CompletionContext::Argument { 149 + prefix: prefix.to_string(), 150 + span, 151 + } 152 + } 153 + } 154 + } 155 + 156 + pub fn handle_block_or_closure( 157 + input: &str, 158 + shapes: &[(Span, FlatShape)], 159 + working_set: &StateWorkingSet, 160 + prefix: &str, 161 + span: Span, 162 + shape_name: &str, 163 + current_idx: usize, 164 + local_span: Span, 165 + global_offset: usize, 166 + ) -> Option<CompletionContext> { 167 + console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}"); 168 + 169 + // Check if the content ends with a pipe or semicolon 170 + let prefix_ends_with_separator = ends_with_separator(prefix); 171 + let last_sep_pos_in_prefix = if prefix_ends_with_separator { 172 + find_last_separator_pos(prefix) 173 + } else { 174 + None 175 + }; 176 + console_log!( 177 + "[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}" 178 + ); 179 + 180 + if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) { 181 + console_log!( 182 + "[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}" 183 + ); 184 + 185 + if is_empty { 186 + // Empty block/closure or just whitespace - command context 187 + console_log!("[completion] {shape_name} is empty, setting Command context"); 188 + Some(CompletionContext::Command { 189 + prefix: String::new(), 190 + span: adjusted_span, 191 + }) 192 + } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { 193 + // After a separator - command context 194 + let after_sep = prefix[last_sep_pos..].trim_start(); 195 + console_log!( 196 + "[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context" 197 + ); 198 + Some(CompletionContext::Command { 199 + prefix: after_sep.to_string(), 200 + span: Span::new(span.start + last_sep_pos, span.end), 201 + }) 202 + } else { 203 + console_log!( 204 + "[completion] {shape_name} has no separator, checking for variable/flag/argument context" 205 + ); 206 + // Check if this is a variable or cell path first 207 + let trimmed = trimmed_prefix.trim(); 208 + 209 + if trimmed.starts_with('$') { 210 + // Variable or cell path completion 211 + if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) { 212 + let var_id = lookup_variable_id(var_name, working_set); 213 + 214 + if let Some(var_id) = var_id { 215 + let prefix_byte_len = cell_prefix.len(); 216 + let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); 217 + console_log!( 218 + "[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}" 219 + ); 220 + Some(CompletionContext::CellPath { 221 + prefix: cell_prefix.to_string(), 222 + span: Span::new(cell_span_start, adjusted_span.end), 223 + var_id, 224 + path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 225 + }) 226 + } else { 227 + // Unknown variable, fall back to variable completion 228 + let var_prefix = trimmed[1..].to_string(); 229 + console_log!( 230 + "[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}" 231 + ); 232 + Some(CompletionContext::Variable { 233 + prefix: var_prefix, 234 + span: adjusted_span, 235 + }) 236 + } 237 + } else { 238 + // Simple variable completion (no dot) 239 + let var_prefix = if trimmed.len() > 1 { 240 + trimmed[1..].to_string() 241 + } else { 242 + String::new() 243 + }; 244 + console_log!( 245 + "[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}" 246 + ); 247 + Some(CompletionContext::Variable { 248 + prefix: var_prefix, 249 + span: adjusted_span, 250 + }) 251 + } 252 + } else if trimmed.starts_with('-') { 253 + // Flag completion 254 + if let Some((cmd_name, _)) = find_command_and_arg_index( 255 + input, 256 + shapes, 257 + current_idx, 258 + local_span, 259 + global_offset, 260 + ) { 261 + console_log!( 262 + "[completion] {shape_name}: Found command {cmd_name:?} for flag completion" 263 + ); 264 + Some(CompletionContext::Flag { 265 + prefix: trimmed.to_string(), 266 + span: adjusted_span, 267 + command_name: cmd_name, 268 + }) 269 + } else { 270 + Some(CompletionContext::Argument { 271 + prefix: trimmed_prefix.to_string(), 272 + span: adjusted_span, 273 + }) 274 + } 275 + } else { 276 + // Try to find the command and argument index 277 + if let Some((cmd_name, arg_index)) = find_command_and_arg_index( 278 + input, 279 + shapes, 280 + current_idx, 281 + local_span, 282 + global_offset, 283 + ) { 284 + console_log!( 285 + "[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion" 286 + ); 287 + Some(CompletionContext::CommandArgument { 288 + prefix: trimmed.to_string(), 289 + span: adjusted_span, 290 + command_name: cmd_name, 291 + arg_index, 292 + }) 293 + } else { 294 + // No command found, treat as regular argument 295 + console_log!( 296 + "[completion] {shape_name}: No command found, using Argument context" 297 + ); 298 + Some(CompletionContext::Argument { 299 + prefix: trimmed_prefix.to_string(), 300 + span: adjusted_span, 301 + }) 302 + } 303 + } 304 + } 305 + } else { 306 + None 307 + } 308 + } 309 + 310 + pub fn handle_variable_string_shape( 311 + input: &str, 312 + shapes: &[(Span, FlatShape)], 313 + _working_set: &StateWorkingSet, 314 + idx: usize, 315 + prefix: &str, 316 + span: Span, 317 + local_span: Span, 318 + global_offset: usize, 319 + ) -> Option<CompletionContext> { 320 + if idx == 0 { 321 + return None; 322 + } 323 + 324 + let prev_shape = &shapes[idx - 1]; 325 + let prev_local_span = to_local_span(prev_shape.0, global_offset); 326 + 327 + if let FlatShape::Variable(var_id) = prev_shape.1 { 328 + // Check if the variable shape ends right where this shape starts (or very close) 329 + // Allow for a small gap (like a dot) between shapes 330 + let gap = local_span.start.saturating_sub(prev_local_span.end); 331 + if gap <= 1 { 332 + // This is a cell path - the String shape contains the field name(s) 333 + // The prefix might be like "na" or "field.subfield" 334 + let trimmed_prefix = prefix.trim(); 335 + let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix); 336 + 337 + let prefix_byte_len = cell_prefix.len(); 338 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 339 + console_log!( 340 + "[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}" 341 + ); 342 + Some(CompletionContext::CellPath { 343 + prefix: cell_prefix.to_string(), 344 + span: Span::new(cell_span_start, span.end), 345 + var_id, 346 + path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 347 + }) 348 + } else { 349 + // Gap between shapes, use helper to determine context 350 + Some(determine_flag_or_argument_context( 351 + input, 352 + shapes, 353 + &prefix.trim(), 354 + idx, 355 + local_span, 356 + span, 357 + global_offset, 358 + )) 359 + } 360 + } else { 361 + // Previous shape is not a Variable, use helper to determine context 362 + Some(determine_flag_or_argument_context( 363 + input, 364 + shapes, 365 + &prefix.trim(), 366 + idx, 367 + local_span, 368 + span, 369 + global_offset, 370 + )) 371 + } 372 + } 373 + 374 + pub fn handle_dot_shape( 375 + _input: &str, 376 + shapes: &[(Span, FlatShape)], 377 + idx: usize, 378 + prefix: &str, 379 + span: Span, 380 + local_span: Span, 381 + global_offset: usize, 382 + ) -> Option<CompletionContext> { 383 + if idx == 0 { 384 + return Some(CompletionContext::Argument { 385 + prefix: prefix.to_string(), 386 + span, 387 + }); 388 + } 389 + 390 + let prev_shape = &shapes[idx - 1]; 391 + let prev_local_span = to_local_span(prev_shape.0, global_offset); 392 + 393 + if let FlatShape::Variable(var_id) = prev_shape.1 { 394 + // Check if the variable shape ends right where this shape starts 395 + if prev_local_span.end == local_span.start { 396 + let trimmed_prefix = prefix.trim(); 397 + // Parse path members from the prefix (which is like ".field" or ".field.subfield") 398 + let after_dot = &trimmed_prefix[1..]; // Remove leading dot 399 + let (path_so_far, cell_prefix) = if after_dot.is_empty() { 400 + (vec![], "") 401 + } else { 402 + parse_cell_path_from_fields(after_dot) 403 + }; 404 + 405 + let prefix_byte_len = cell_prefix.len(); 406 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 407 + console_log!( 408 + "[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}" 409 + ); 410 + Some(CompletionContext::CellPath { 411 + prefix: cell_prefix.to_string(), 412 + span: Span::new(cell_span_start, span.end), 413 + var_id, 414 + path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 415 + }) 416 + } else { 417 + // Gap between shapes, fall through to default handling 418 + Some(CompletionContext::Argument { 419 + prefix: prefix.to_string(), 420 + span, 421 + }) 422 + } 423 + } else { 424 + // Previous shape is not a Variable, this is likely a file path starting with . 425 + Some(CompletionContext::Argument { 426 + prefix: prefix.to_string(), 427 + span, 428 + }) 429 + } 430 + } 431 + 432 + pub fn determine_context_from_shape( 433 + input: &str, 434 + shapes: &[(Span, FlatShape)], 435 + working_set: &StateWorkingSet, 436 + byte_pos: usize, 437 + global_offset: usize, 438 + ) -> Option<CompletionContext> { 439 + // First, check if cursor is within a shape 440 + for (idx, (span, shape)) in shapes.iter().enumerate() { 441 + let local_span = to_local_span(*span, global_offset); 442 + 443 + if local_span.start <= byte_pos && byte_pos <= local_span.end { 444 + console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}"); 445 + 446 + // Check if there's a pipe or semicolon between this shape's end and the cursor 447 + // If so, we're starting a new command and should ignore this shape 448 + let has_sep = has_separator_between(input, local_span.end, byte_pos); 449 + if has_sep { 450 + console_log!( 451 + "[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape", 452 + end = local_span.end 453 + ); 454 + // There's a separator, so we're starting a new command - skip this shape 455 + continue; 456 + } 457 + 458 + let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos)); 459 + let prefix = safe_slice(input, span); 460 + console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}"); 461 + 462 + // Special case: if prefix is just '{' (possibly with whitespace), 463 + // we're at the start of a block and should complete commands 464 + let trimmed_prefix = prefix.trim(); 465 + if trimmed_prefix == "{" { 466 + // We're right after '{' - command context 467 + if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) { 468 + return Some(CompletionContext::Command { 469 + prefix: String::new(), 470 + span: adjusted_span, 471 + }); 472 + } 473 + } else { 474 + match shape { 475 + // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes 476 + _ if { idx > 0 && matches!(shape, FlatShape::String) } => { 477 + if let Some(ctx) = handle_variable_string_shape( 478 + input, 479 + shapes, 480 + working_set, 481 + idx, 482 + &prefix, 483 + span, 484 + local_span, 485 + global_offset, 486 + ) { 487 + return Some(ctx); 488 + } 489 + } 490 + // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes 491 + _ if { 492 + let trimmed_prefix = prefix.trim(); 493 + trimmed_prefix.starts_with('.') && idx > 0 494 + } => 495 + { 496 + if let Some(ctx) = handle_dot_shape( 497 + input, 498 + shapes, 499 + idx, 500 + &prefix, 501 + span, 502 + local_span, 503 + global_offset, 504 + ) { 505 + return Some(ctx); 506 + } 507 + } 508 + _ if { 509 + // Check if this is a variable or cell path (starts with $) before treating as command 510 + let trimmed_prefix = prefix.trim(); 511 + trimmed_prefix.starts_with('$') 512 + } => 513 + { 514 + let trimmed_prefix = prefix.trim(); 515 + // Check if this is a cell path (contains a dot after $) 516 + if let Some((var_name, path_so_far, cell_prefix)) = 517 + parse_cell_path(trimmed_prefix) 518 + { 519 + // Find the variable ID 520 + let var_id = lookup_variable_id(var_name, working_set); 521 + 522 + if let Some(var_id) = var_id { 523 + // Calculate span for the cell path member being completed 524 + let prefix_byte_len = cell_prefix.len(); 525 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 526 + return Some(CompletionContext::CellPath { 527 + prefix: cell_prefix.to_string(), 528 + span: Span::new(cell_span_start, span.end), 529 + var_id, 530 + path_so_far: path_so_far 531 + .iter() 532 + .map(|s| s.to_string()) 533 + .collect(), 534 + }); 535 + } else { 536 + // Unknown variable, fall back to variable completion 537 + let var_prefix = trimmed_prefix[1..].to_string(); 538 + return Some(CompletionContext::Variable { 539 + prefix: var_prefix, 540 + span, 541 + }); 542 + } 543 + } else { 544 + // Variable completion context (no dot) 545 + let var_prefix = if trimmed_prefix.len() > 1 { 546 + trimmed_prefix[1..].to_string() 547 + } else { 548 + String::new() 549 + }; 550 + return Some(CompletionContext::Variable { 551 + prefix: var_prefix, 552 + span, 553 + }); 554 + } 555 + } 556 + _ if is_command_shape(input, shape, local_span) => { 557 + let (full_prefix, full_span) = 558 + build_command_prefix(input, shapes, idx, span, &prefix, global_offset); 559 + return Some(CompletionContext::Command { 560 + prefix: full_prefix, 561 + span: full_span, 562 + }); 563 + } 564 + FlatShape::Block | FlatShape::Closure => { 565 + if let Some(ctx) = handle_block_or_closure( 566 + input, 567 + shapes, 568 + working_set, 569 + &prefix, 570 + span, 571 + shape.as_str().trim_start_matches("shape_"), 572 + idx, 573 + local_span, 574 + global_offset, 575 + ) { 576 + return Some(ctx); 577 + } 578 + } 579 + FlatShape::Variable(var_id) => { 580 + // Variable or cell path completion context 581 + let trimmed_prefix = prefix.trim(); 582 + if trimmed_prefix.starts_with('$') { 583 + // Check if this is a cell path (contains a dot after $) 584 + if let Some((_, path_so_far, cell_prefix)) = 585 + parse_cell_path(trimmed_prefix) 586 + { 587 + let prefix_byte_len = cell_prefix.len(); 588 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 589 + return Some(CompletionContext::CellPath { 590 + prefix: cell_prefix.to_string(), 591 + span: Span::new(cell_span_start, span.end), 592 + var_id: *var_id, 593 + path_so_far: path_so_far 594 + .iter() 595 + .map(|s| s.to_string()) 596 + .collect(), 597 + }); 598 + } else { 599 + // Simple variable completion 600 + let var_prefix = trimmed_prefix[1..].to_string(); 601 + return Some(CompletionContext::Variable { 602 + prefix: var_prefix, 603 + span, 604 + }); 605 + } 606 + } else { 607 + // Fallback to argument context if no $ found 608 + return Some(CompletionContext::Argument { 609 + prefix: prefix.to_string(), 610 + span, 611 + }); 612 + } 613 + } 614 + _ => { 615 + // Check if this is a variable or cell path (starts with $) 616 + let trimmed_prefix = prefix.trim(); 617 + if trimmed_prefix.starts_with('$') { 618 + // Check if this is a cell path (contains a dot after $) 619 + if let Some((var_name, path_so_far, cell_prefix)) = 620 + parse_cell_path(trimmed_prefix) 621 + { 622 + let var_id = lookup_variable_id(var_name, working_set); 623 + if let Some(var_id) = var_id { 624 + let prefix_byte_len = cell_prefix.len(); 625 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 626 + return Some(CompletionContext::CellPath { 627 + prefix: cell_prefix.to_string(), 628 + span: Span::new(cell_span_start, span.end), 629 + var_id, 630 + path_so_far: path_so_far 631 + .iter() 632 + .map(|s| s.to_string()) 633 + .collect(), 634 + }); 635 + } else { 636 + let var_prefix = trimmed_prefix[1..].to_string(); 637 + return Some(CompletionContext::Variable { 638 + prefix: var_prefix, 639 + span, 640 + }); 641 + } 642 + } else { 643 + // Simple variable completion 644 + let var_prefix = if trimmed_prefix.len() > 1 { 645 + trimmed_prefix[1..].to_string() 646 + } else { 647 + String::new() 648 + }; 649 + return Some(CompletionContext::Variable { 650 + prefix: var_prefix, 651 + span, 652 + }); 653 + } 654 + } else { 655 + // Use helper to determine flag or argument context 656 + return Some(determine_flag_or_argument_context( 657 + input, 658 + shapes, 659 + &trimmed_prefix, 660 + idx, 661 + local_span, 662 + span, 663 + global_offset, 664 + )); 665 + } 666 + } 667 + } 668 + } 669 + break; 670 + } 671 + } 672 + None 673 + } 674 + 675 + pub fn determine_context_fallback( 676 + input: &str, 677 + shapes: &[(Span, FlatShape)], 678 + working_set: &StateWorkingSet, 679 + engine_guard: &EngineState, 680 + byte_pos: usize, 681 + global_offset: usize, 682 + ) -> Option<CompletionContext> { 683 + use nu_parser::{TokenContents, lex}; 684 + 685 + console_log!("[completion] Context is None, entering fallback logic"); 686 + // Check if there's a command-like shape before us 687 + let mut has_separator_after_command = false; 688 + for (span, shape) in shapes.iter().rev() { 689 + let local_span = to_local_span(*span, global_offset); 690 + if local_span.end <= byte_pos { 691 + if is_command_shape(input, shape, local_span) { 692 + // Check if there's a pipe or semicolon between this command and the cursor 693 + has_separator_after_command = 694 + has_separator_between(input, local_span.end, byte_pos); 695 + console_log!( 696 + "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}" 697 + ); 698 + if !has_separator_after_command { 699 + // Extract the command text 700 + let cmd = safe_slice(input, local_span); 701 + let cmd_name = extract_command_name(cmd).to_string(); 702 + 703 + // Check if we're right after the command (only whitespace between command and cursor) 704 + let text_after_command = if local_span.end < input.len() { 705 + &input[local_span.end..byte_pos] 706 + } else { 707 + "" 708 + }; 709 + let is_right_after_command = text_after_command.trim().is_empty(); 710 + 711 + // If we're right after a command, check if it has positional arguments 712 + if is_right_after_command { 713 + if let Some(signature) = get_command_signature(engine_guard, &cmd_name) { 714 + // Check if command has any positional arguments 715 + let has_positional_args = !signature.required_positional.is_empty() 716 + || !signature.optional_positional.is_empty(); 717 + 718 + if has_positional_args { 719 + // Count existing arguments before cursor 720 + let mut arg_count = 0; 721 + for (prev_span, prev_shape) in shapes.iter().rev() { 722 + let prev_local_span = to_local_span(*prev_span, global_offset); 723 + if prev_local_span.end <= byte_pos 724 + && prev_local_span.end > local_span.end 725 + { 726 + if !is_command_shape(input, prev_shape, prev_local_span) { 727 + let arg_text = safe_slice(input, prev_local_span); 728 + let trimmed_arg = arg_text.trim(); 729 + // Don't count flags (starting with -) or empty arguments 730 + if !trimmed_arg.is_empty() 731 + && !trimmed_arg.starts_with('-') 732 + { 733 + arg_count += 1; 734 + } 735 + } 736 + } 737 + } 738 + 739 + console_log!( 740 + "[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}" 741 + ); 742 + 743 + return Some(CompletionContext::CommandArgument { 744 + prefix: String::new(), 745 + span: Span::new(byte_pos, byte_pos), 746 + command_name: cmd_name, 747 + arg_index: arg_count, 748 + }); 749 + } else { 750 + // No positional arguments, don't show any completions 751 + console_log!( 752 + "[completion] Command {cmd_name:?} has no positional args, not showing completions" 753 + ); 754 + // Leave context as None to show no completions 755 + return None; 756 + } 757 + } else { 758 + // Couldn't find signature, don't show completions 759 + console_log!( 760 + "[completion] Could not find signature for {cmd_name:?}, not showing completions" 761 + ); 762 + // Leave context as None to show no completions 763 + return None; 764 + } 765 + } else { 766 + // Not right after command, complete the command itself 767 + console_log!("[completion] Set Command context with prefix: {cmd:?}"); 768 + return Some(CompletionContext::Command { 769 + prefix: cmd.to_string(), 770 + span: local_span, 771 + }); 772 + } 773 + } 774 + } 775 + break; 776 + } 777 + } 778 + 779 + // No command found before, check context from tokens 780 + console_log!("[completion] No command found before cursor, checking tokens"); 781 + // No command before, check context from tokens 782 + let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true); 783 + let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 784 + 785 + let is_cmd_context = if let Some(token) = last_token { 786 + let matches = matches!( 787 + token.contents, 788 + TokenContents::Pipe 789 + | TokenContents::PipePipe 790 + | TokenContents::Semicolon 791 + | TokenContents::Eol 792 + ); 793 + console_log!( 794 + "[completion] Last token: {contents:?}, is_cmd_context from token={matches}", 795 + contents = token.contents 796 + ); 797 + matches 798 + } else { 799 + console_log!( 800 + "[completion] No last token found, assuming start of input (is_cmd_context=true)" 801 + ); 802 + true // Start of input 803 + }; 804 + 805 + // Look for the last non-whitespace token before cursor 806 + let text_before = &input[..byte_pos]; 807 + 808 + // Also check if we're inside a block - if the last non-whitespace char before cursor is '{' 809 + let text_before_trimmed = text_before.trim_end(); 810 + let is_inside_block = text_before_trimmed.ends_with('{'); 811 + // If we found a separator after a command, we're starting a new command 812 + let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command; 813 + console_log!( 814 + "[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}" 815 + ); 816 + 817 + // Find the last word before cursor 818 + let last_word_start = text_before 819 + .rfind(|c: char| c.is_whitespace() || is_separator_char(c)) 820 + .map(|i| i + 1) 821 + .unwrap_or(0); 822 + 823 + let last_word = text_before[last_word_start..].trim_start(); 824 + console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}"); 825 + 826 + if is_cmd_context { 827 + Some(CompletionContext::Command { 828 + prefix: last_word.to_string(), 829 + span: Span::new(last_word_start, byte_pos), 830 + }) 831 + } else { 832 + // Check if this is a variable or cell path (starts with $) 833 + let trimmed_word = last_word.trim(); 834 + if trimmed_word.starts_with('$') { 835 + // Check if this is a cell path (contains a dot after $) 836 + if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) { 837 + let var_id = lookup_variable_id(&var_name, working_set); 838 + 839 + if let Some(var_id) = var_id { 840 + let prefix_byte_len = cell_prefix.len(); 841 + let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 842 + Some(CompletionContext::CellPath { 843 + prefix: cell_prefix.to_string(), 844 + span: Span::new(cell_span_start, byte_pos), 845 + var_id, 846 + path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 847 + }) 848 + } else { 849 + let var_prefix = trimmed_word[1..].to_string(); 850 + Some(CompletionContext::Variable { 851 + prefix: var_prefix, 852 + span: Span::new(last_word_start, byte_pos), 853 + }) 854 + } 855 + } else { 856 + // Simple variable completion 857 + let var_prefix = trimmed_word[1..].to_string(); 858 + Some(CompletionContext::Variable { 859 + prefix: var_prefix, 860 + span: Span::new(last_word_start, byte_pos), 861 + }) 862 + } 863 + } else if trimmed_word.starts_with('-') { 864 + // Try to find command by looking backwards through shapes 865 + let mut found_cmd = None; 866 + for (span, shape) in shapes.iter().rev() { 867 + let local_span = to_local_span(*span, global_offset); 868 + if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) { 869 + let cmd_text = safe_slice(input, local_span); 870 + let cmd_name = extract_command_name(cmd_text).to_string(); 871 + found_cmd = Some(cmd_name); 872 + break; 873 + } 874 + } 875 + if let Some(cmd_name) = found_cmd { 876 + Some(CompletionContext::Flag { 877 + prefix: trimmed_word.to_string(), 878 + span: Span::new(last_word_start, byte_pos), 879 + command_name: cmd_name, 880 + }) 881 + } else { 882 + Some(CompletionContext::Argument { 883 + prefix: last_word.to_string(), 884 + span: Span::new(last_word_start, byte_pos), 885 + }) 886 + } 887 + } else { 888 + // Try to find command and argument index 889 + let mut found_cmd = None; 890 + let mut arg_count = 0; 891 + for (span, shape) in shapes.iter().rev() { 892 + let local_span = to_local_span(*span, global_offset); 893 + if local_span.end <= byte_pos { 894 + if is_command_shape(input, shape, local_span) { 895 + let cmd_text = safe_slice(input, local_span); 896 + let cmd_name = extract_command_name(cmd_text).to_string(); 897 + found_cmd = Some(cmd_name); 898 + break; 899 + } else { 900 + let arg_text = safe_slice(input, local_span); 901 + let trimmed_arg = arg_text.trim(); 902 + if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 903 + arg_count += 1; 904 + } 905 + } 906 + } 907 + } 908 + if let Some(cmd_name) = found_cmd { 909 + Some(CompletionContext::CommandArgument { 910 + prefix: trimmed_word.to_string(), 911 + span: Span::new(last_word_start, byte_pos), 912 + command_name: cmd_name, 913 + arg_index: arg_count, 914 + }) 915 + } else { 916 + Some(CompletionContext::Argument { 917 + prefix: last_word.to_string(), 918 + span: Span::new(last_word_start, byte_pos), 919 + }) 920 + } 921 + } 922 + } 923 + } 924 + 925 + pub fn determine_context( 926 + input: &str, 927 + shapes: &[(Span, FlatShape)], 928 + working_set: &StateWorkingSet, 929 + engine_guard: &EngineState, 930 + byte_pos: usize, 931 + global_offset: usize, 932 + ) -> Option<CompletionContext> { 933 + // First try to determine context from shapes 934 + if let Some(ctx) = 935 + determine_context_from_shape(input, shapes, working_set, byte_pos, global_offset) 936 + { 937 + return Some(ctx); 938 + } 939 + 940 + // Fallback to token-based context determination 941 + determine_context_fallback( 942 + input, 943 + shapes, 944 + working_set, 945 + engine_guard, 946 + byte_pos, 947 + global_offset, 948 + ) 949 + }
+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 + }
+168
src/completion/helpers.rs
··· 1 + use nu_parser::FlatShape; 2 + use nu_protocol::engine::StateWorkingSet; 3 + use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span}; 4 + 5 + /// Macro for console logging that automatically converts formatted strings to JsValue 6 + #[macro_export] 7 + macro_rules! console_log { 8 + ($($arg:tt)*) => { 9 + web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!($($arg)*))); 10 + }; 11 + } 12 + 13 + pub fn is_separator_char(c: char) -> bool { 14 + ['|', ';', '(', '{'].contains(&c) 15 + } 16 + 17 + pub fn is_command_separator_char(c: char) -> bool { 18 + ['|', ';'].contains(&c) 19 + } 20 + 21 + pub fn has_separator_between(input: &str, start: usize, end: usize) -> bool { 22 + if start < end && start < input.len() { 23 + let text_between = &input[start..std::cmp::min(end, input.len())]; 24 + text_between.chars().any(|c| is_separator_char(c)) 25 + } else { 26 + false 27 + } 28 + } 29 + 30 + pub fn find_last_separator_pos(text: &str) -> Option<usize> { 31 + text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1) 32 + } 33 + 34 + pub fn ends_with_separator(text: &str) -> bool { 35 + let text = text.trim_end(); 36 + text.ends_with('|') || text.ends_with(';') 37 + } 38 + 39 + pub fn to_local_span(span: Span, global_offset: usize) -> Span { 40 + Span::new( 41 + span.start.saturating_sub(global_offset), 42 + span.end.saturating_sub(global_offset), 43 + ) 44 + } 45 + 46 + pub fn safe_slice(input: &str, span: Span) -> &str { 47 + if span.start < input.len() { 48 + let safe_end = std::cmp::min(span.end, input.len()); 49 + &input[span.start..safe_end] 50 + } else { 51 + "" 52 + } 53 + } 54 + 55 + pub fn is_command_shape(input: &str, shape: &FlatShape, local_span: Span) -> bool { 56 + matches!( 57 + shape, 58 + FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword 59 + ) || matches!(shape, FlatShape::Garbage) && { 60 + if local_span.start < input.len() { 61 + let prev_text = safe_slice(input, local_span); 62 + !prev_text.trim().starts_with('-') 63 + } else { 64 + false 65 + } 66 + } 67 + } 68 + 69 + pub fn handle_block_prefix(prefix: &str, span: Span) -> Option<(&str, Span, bool)> { 70 + let mut block_prefix = prefix; 71 + let mut block_span_start = span.start; 72 + 73 + // Remove leading '{' and whitespace 74 + if block_prefix.starts_with('{') { 75 + block_prefix = &block_prefix[1..]; 76 + block_span_start += 1; 77 + } 78 + let trimmed_block_prefix = block_prefix.trim_start(); 79 + if trimmed_block_prefix != block_prefix { 80 + // Adjust span start to skip whitespace 81 + block_span_start += block_prefix.len() - trimmed_block_prefix.len(); 82 + } 83 + 84 + let is_empty = trimmed_block_prefix.is_empty(); 85 + Some(( 86 + trimmed_block_prefix, 87 + Span::new(block_span_start, span.end), 88 + is_empty, 89 + )) 90 + } 91 + 92 + pub fn extract_command_name(cmd_text: &str) -> &str { 93 + cmd_text 94 + .split_whitespace() 95 + .next() 96 + .unwrap_or(cmd_text) 97 + .trim() 98 + } 99 + 100 + pub fn lookup_variable_id( 101 + var_name: &str, 102 + working_set: &StateWorkingSet, 103 + ) -> Option<nu_protocol::VarId> { 104 + match var_name { 105 + "env" => Some(ENV_VARIABLE_ID), 106 + "nu" => Some(NU_VARIABLE_ID), 107 + "in" => Some(IN_VARIABLE_ID), 108 + _ => working_set.find_variable(var_name.as_bytes()), 109 + } 110 + } 111 + 112 + pub fn parse_cell_path(text: &str) -> Option<(&str, Vec<&str>, &str)> { 113 + let trimmed = text.trim(); 114 + if !trimmed.starts_with('$') { 115 + return None; 116 + } 117 + 118 + // Check if this is a cell path (contains a dot after $) 119 + if let Some(dot_pos) = trimmed[1..].find('.') { 120 + let var_name = &trimmed[1..dot_pos + 1]; 121 + let after_var = &trimmed[dot_pos + 2..]; 122 + let parts: Vec<&str> = after_var.split('.').collect(); 123 + let (path_so_far, cell_prefix) = if parts.is_empty() { 124 + (vec![], "") 125 + } else if after_var.ends_with('.') { 126 + ( 127 + parts.iter().filter(|s| !s.is_empty()).copied().collect(), 128 + "", 129 + ) 130 + } else { 131 + let path: Vec<&str> = parts[..parts.len().saturating_sub(1)] 132 + .iter() 133 + .copied() 134 + .collect(); 135 + let prefix = parts.last().copied().unwrap_or(""); 136 + (path, prefix) 137 + }; 138 + Some((var_name, path_so_far, cell_prefix)) 139 + } else { 140 + None 141 + } 142 + } 143 + 144 + pub fn parse_cell_path_from_fields(text: &str) -> (Vec<&str>, &str) { 145 + let trimmed = text.trim(); 146 + let parts: Vec<&str> = trimmed.split('.').collect(); 147 + if parts.is_empty() { 148 + (vec![], "") 149 + } else if trimmed.ends_with('.') { 150 + ( 151 + parts.iter().filter(|s| !s.is_empty()).copied().collect(), 152 + "", 153 + ) 154 + } else { 155 + let path: Vec<&str> = parts[..parts.len().saturating_sub(1)] 156 + .iter() 157 + .copied() 158 + .collect(); 159 + let prefix = parts.last().copied().unwrap_or(""); 160 + (path, prefix) 161 + } 162 + } 163 + 164 + pub fn to_char_span(input: &str, span: Span) -> Span { 165 + let char_start = input[..span.start].chars().count(); 166 + let char_end = input[..span.end].chars().count(); 167 + Span::new(char_start, char_end) 168 + }
+95
src/completion/mod.rs
··· 1 + use crate::console_log; 2 + use futures::FutureExt; 3 + use js_sys::Promise; 4 + use nu_parser::{flatten_block, parse}; 5 + use nu_protocol::engine::StateWorkingSet; 6 + use wasm_bindgen::prelude::*; 7 + use wasm_bindgen_futures::future_to_promise; 8 + 9 + use super::*; 10 + 11 + pub mod context; 12 + pub mod files; 13 + pub mod helpers; 14 + pub mod suggestions; 15 + pub mod types; 16 + pub mod variables; 17 + 18 + pub use context::determine_context; 19 + pub use suggestions::generate_suggestions; 20 + pub use types::{CompletionContext, Suggestion}; 21 + 22 + #[wasm_bindgen] 23 + pub fn completion(input: String, js_cursor_pos: usize) -> Promise { 24 + future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s)))) 25 + } 26 + 27 + pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { 28 + let engine_guard = read_engine_state().await; 29 + let stack_guard = crate::read_stack().await; 30 + let root = get_pwd(); 31 + 32 + // Map UTF-16 cursor position (from JS) to Byte index (for Rust) 33 + let byte_pos = input 34 + .char_indices() 35 + .map(|(i, _)| i) 36 + .nth(js_cursor_pos) 37 + .unwrap_or(input.len()); 38 + 39 + let (working_set, shapes, global_offset) = { 40 + let mut working_set = StateWorkingSet::new(&engine_guard); 41 + let global_offset = working_set.next_span_start(); 42 + let block = parse(&mut working_set, None, input.as_bytes(), false); 43 + let shapes = flatten_block(&working_set, &block); 44 + (working_set, shapes, global_offset) 45 + }; 46 + 47 + // Initial state logging 48 + console_log!( 49 + "[completion] Input: {input:?}, JS cursor: {js_cursor_pos}, byte cursor: {byte_pos}" 50 + ); 51 + console_log!( 52 + "[completion] Found {count} shapes, global_offset: {global_offset}", 53 + count = shapes.len() 54 + ); 55 + for (idx, (span, shape)) in shapes.iter().enumerate() { 56 + let (local_start, local_end) = ( 57 + span.start.saturating_sub(global_offset), 58 + span.end.saturating_sub(global_offset), 59 + ); 60 + console_log!( 61 + "[completion] Shape {idx}: {shape:?} at [{start}, {end}] (local: [{local_start}, {local_end}])", 62 + start = span.start, 63 + end = span.end 64 + ); 65 + } 66 + 67 + // Determine completion context 68 + let context = determine_context( 69 + &input, 70 + &shapes, 71 + &working_set, 72 + &engine_guard, 73 + byte_pos, 74 + global_offset, 75 + ); 76 + 77 + // Generate suggestions based on context 78 + let mut suggestions = generate_suggestions( 79 + &input, 80 + context, 81 + &working_set, 82 + &engine_guard, 83 + &stack_guard, 84 + &root, 85 + byte_pos, 86 + ); 87 + 88 + drop(working_set); 89 + drop(engine_guard); 90 + 91 + suggestions.sort(); 92 + let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()); 93 + console_log!("{suggestions}"); 94 + suggestions 95 + }
+384
src/completion/suggestions.rs
··· 1 + use crate::completion::context::get_command_signature; 2 + use crate::completion::files::generate_file_suggestions; 3 + use crate::completion::helpers::to_char_span; 4 + use crate::completion::types::{CompletionContext, Suggestion}; 5 + use crate::completion::variables::*; 6 + use crate::console_log; 7 + use nu_protocol::Span; 8 + use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; 9 + 10 + pub fn generate_command_suggestions( 11 + input: &str, 12 + working_set: &StateWorkingSet, 13 + prefix: String, 14 + span: Span, 15 + ) -> Vec<Suggestion> { 16 + console_log!("[completion] Generating Command suggestions with prefix: {prefix:?}"); 17 + // Command completion 18 + let cmds = 19 + working_set.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); 20 + 21 + let span = to_char_span(input, span); 22 + let mut suggestions = Vec::new(); 23 + let mut cmd_count = 0; 24 + 25 + for (_, name, desc, _) in cmds { 26 + let name_str = String::from_utf8_lossy(&name).to_string(); 27 + suggestions.push(Suggestion { 28 + rendered: { 29 + let name_colored = ansi_term::Color::Green.bold().paint(&name_str); 30 + let desc_str = desc.as_deref().unwrap_or("<no description>"); 31 + format!("{name_colored} {desc_str}") 32 + }, 33 + name: name_str, 34 + description: desc, 35 + is_command: true, 36 + span_start: span.start, 37 + span_end: span.end, 38 + }); 39 + cmd_count += 1; 40 + } 41 + console_log!("[completion] Found {cmd_count} command suggestions"); 42 + suggestions 43 + } 44 + 45 + pub fn generate_argument_suggestions( 46 + input: &str, 47 + prefix: String, 48 + span: Span, 49 + root: &std::sync::Arc<vfs::VfsPath>, 50 + ) -> Vec<Suggestion> { 51 + console_log!("[completion] Generating Argument suggestions with prefix: {prefix:?}"); 52 + // File completion 53 + let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 54 + let file_count = file_suggestions.len(); 55 + console_log!("[completion] Found {file_count} file suggestions"); 56 + file_suggestions 57 + } 58 + 59 + pub fn generate_flag_suggestions( 60 + input: &str, 61 + engine_guard: &EngineState, 62 + prefix: String, 63 + span: Span, 64 + command_name: String, 65 + ) -> Vec<Suggestion> { 66 + console_log!( 67 + "[completion] Generating Flag suggestions for command: {command_name:?}, prefix: {prefix:?}" 68 + ); 69 + 70 + let mut suggestions = Vec::new(); 71 + if let Some(signature) = get_command_signature(engine_guard, &command_name) { 72 + let span = to_char_span(input, span); 73 + let mut flag_count = 0; 74 + 75 + // Get switches from signature 76 + // Signature has a named field that contains named arguments (including switches) 77 + for flag in &signature.named { 78 + // Check if this is a switch (has no argument) 79 + // Switches have arg: None, named arguments have arg: Some(SyntaxShape) 80 + let is_switch = flag.arg.is_none(); 81 + 82 + if is_switch { 83 + let long_name = format!("--{}", flag.long); 84 + let short_name = flag.short.map(|c| format!("-{}", c)); 85 + 86 + // Determine which flags to show based on prefix: 87 + // - If prefix is empty or exactly "-", show all flags (both short and long) 88 + // - If prefix starts with "--", only show long flags that match the prefix 89 + // - If prefix starts with "-" (but not "--"), only show short flags that match the prefix 90 + let show_all = prefix.is_empty() || prefix == "-"; 91 + 92 + // Helper to create a flag suggestion 93 + let create_flag_suggestion = |flag_name: String| -> Suggestion { 94 + Suggestion { 95 + name: flag_name.clone(), 96 + description: Some(flag.desc.clone()), 97 + is_command: false, 98 + rendered: { 99 + let flag_colored = ansi_term::Color::Cyan.bold().paint(&flag_name); 100 + format!("{flag_colored} {}", flag.desc) 101 + }, 102 + span_start: span.start, 103 + span_end: span.end, 104 + } 105 + }; 106 + 107 + // Add long flag if it matches 108 + let should_show_long = if show_all { 109 + true // Show all flags when prefix is "-" or empty 110 + } else if prefix.starts_with("--") { 111 + long_name.starts_with(&prefix) // Only show long flags matching prefix 112 + } else { 113 + false // Don't show long flags if prefix is short flag format 114 + }; 115 + 116 + if should_show_long { 117 + suggestions.push(create_flag_suggestion(long_name)); 118 + flag_count += 1; 119 + } 120 + 121 + // Add short flag if it matches 122 + if let Some(short) = &short_name { 123 + let should_show_short = if show_all { 124 + true // Show all flags when prefix is "-" or empty 125 + } else if prefix.starts_with("-") && !prefix.starts_with("--") { 126 + short.starts_with(&prefix) // Only show short flags matching prefix 127 + } else { 128 + false // Don't show short flags if prefix is long flag format 129 + }; 130 + 131 + if should_show_short { 132 + suggestions.push(create_flag_suggestion(short.clone())); 133 + flag_count += 1; 134 + } 135 + } 136 + } 137 + } 138 + 139 + console_log!("[completion] Found {flag_count} flag suggestions"); 140 + } else { 141 + console_log!("[completion] Could not find signature for command: {command_name:?}"); 142 + } 143 + suggestions 144 + } 145 + 146 + pub fn generate_command_argument_suggestions( 147 + input: &str, 148 + engine_guard: &EngineState, 149 + prefix: String, 150 + span: Span, 151 + command_name: String, 152 + arg_index: usize, 153 + root: &std::sync::Arc<vfs::VfsPath>, 154 + ) -> Vec<Suggestion> { 155 + console_log!( 156 + "[completion] Generating CommandArgument suggestions for command: {command_name:?}, arg_index: {arg_index}, prefix: {prefix:?}" 157 + ); 158 + 159 + let mut suggestions = Vec::new(); 160 + if let Some(signature) = get_command_signature(engine_guard, &command_name) { 161 + // Get positional arguments from signature 162 + // Combine required and optional positional arguments 163 + let mut all_positional = Vec::new(); 164 + all_positional.extend_from_slice(&signature.required_positional); 165 + all_positional.extend_from_slice(&signature.optional_positional); 166 + 167 + // Find the argument at the given index 168 + if let Some(arg) = all_positional.get(arg_index) { 169 + // Check the SyntaxShape to determine completion type 170 + // Only suggest files/dirs for Filepath type (or "any" when type is unknown) 171 + match &arg.shape { 172 + nu_protocol::SyntaxShape::Filepath | nu_protocol::SyntaxShape::Any => { 173 + // File/directory completion 174 + let file_suggestions = generate_file_suggestions( 175 + &prefix, 176 + span, 177 + root, 178 + Some(arg.desc.clone()), 179 + input, 180 + ); 181 + let file_count = file_suggestions.len(); 182 + suggestions.extend(file_suggestions); 183 + console_log!( 184 + "[completion] Found {file_count} file suggestions for argument {arg_index}" 185 + ); 186 + } 187 + _ => { 188 + // For other types, don't suggest files 189 + console_log!( 190 + "[completion] Argument {arg_index} is type {:?}, not suggesting files", 191 + arg.shape 192 + ); 193 + } 194 + } 195 + } else { 196 + // Argument index out of range, fall back to file completion 197 + console_log!( 198 + "[completion] Argument index {arg_index} out of range, using file completion" 199 + ); 200 + // Use the same file completion logic as Argument context 201 + let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 202 + suggestions.extend(file_suggestions); 203 + } 204 + } else { 205 + // No signature found, fall back to file completion 206 + console_log!( 207 + "[completion] Could not find signature for command: {command_name:?}, using file completion" 208 + ); 209 + let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 210 + suggestions.extend(file_suggestions); 211 + } 212 + suggestions 213 + } 214 + 215 + pub fn generate_variable_suggestions( 216 + input: &str, 217 + working_set: &StateWorkingSet, 218 + prefix: String, 219 + span: Span, 220 + byte_pos: usize, 221 + ) -> Vec<Suggestion> { 222 + console_log!("[completion] Generating Variable suggestions with prefix: {prefix:?}"); 223 + 224 + // Collect all available variables 225 + let variables = collect_variables(working_set, input, byte_pos); 226 + let span = to_char_span(input, span); 227 + let mut suggestions = Vec::new(); 228 + let mut var_count = 0; 229 + 230 + for (var_name, var_id) in variables { 231 + // Filter by prefix (variable name includes $, so we need to check after $) 232 + if var_name.len() > 1 && var_name[1..].starts_with(&prefix) { 233 + // Get variable type 234 + let var_type = working_set.get_variable(var_id).ty.to_string(); 235 + 236 + suggestions.push(Suggestion { 237 + name: var_name.clone(), 238 + description: Some(var_type.clone()), 239 + is_command: false, 240 + rendered: { 241 + let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); 242 + format!("{var_colored} {var_type}") 243 + }, 244 + span_start: span.start, 245 + span_end: span.end, 246 + }); 247 + var_count += 1; 248 + } 249 + } 250 + 251 + console_log!("[completion] Found {var_count} variable suggestions"); 252 + suggestions 253 + } 254 + 255 + pub fn generate_cell_path_suggestions( 256 + input: &str, 257 + working_set: &StateWorkingSet, 258 + engine_guard: &EngineState, 259 + stack_guard: &Stack, 260 + prefix: String, 261 + span: Span, 262 + var_id: nu_protocol::VarId, 263 + path_so_far: Vec<String>, 264 + ) -> Vec<Suggestion> { 265 + console_log!( 266 + "[completion] Generating CellPath suggestions with prefix: {prefix:?}, path: {path_so_far:?}" 267 + ); 268 + 269 + let mut suggestions = Vec::new(); 270 + // Evaluate the variable to get its value 271 + if let Some(var_value) = 272 + eval_variable_for_completion(var_id, working_set, engine_guard, stack_guard) 273 + { 274 + // Follow the path to get the value at the current level 275 + let current_value = if path_so_far.is_empty() { 276 + var_value 277 + } else { 278 + let path_refs: Vec<&str> = path_so_far.iter().map(|s| s.as_str()).collect(); 279 + follow_cell_path(&var_value, &path_refs).unwrap_or(var_value) 280 + }; 281 + 282 + // Get columns/fields from the current value 283 + let columns = get_columns_from_value(&current_value); 284 + let span = to_char_span(input, span); 285 + let mut field_count = 0; 286 + 287 + for (col_name, col_type) in columns { 288 + // Filter by prefix 289 + if col_name.starts_with(&prefix) { 290 + let type_str = col_type.as_deref().unwrap_or("any"); 291 + suggestions.push(Suggestion { 292 + name: col_name.clone(), 293 + description: Some(type_str.to_string()), 294 + is_command: false, 295 + rendered: { 296 + let col_colored = ansi_term::Color::Yellow.paint(&col_name); 297 + format!("{col_colored} {type_str}") 298 + }, 299 + span_start: span.start, 300 + span_end: span.end, 301 + }); 302 + field_count += 1; 303 + } 304 + } 305 + 306 + console_log!("[completion] Found {field_count} cell path suggestions"); 307 + } else { 308 + // Variable couldn't be evaluated - this is expected for runtime variables 309 + // We can't provide cell path completions without knowing the structure 310 + console_log!( 311 + "[completion] Could not evaluate variable {var_id:?} for cell path completion (runtime variable)" 312 + ); 313 + 314 + // Try to get type information to provide better feedback 315 + if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 316 + working_set.get_variable(var_id) 317 + })) { 318 + console_log!("[completion] Variable type: {ty:?}", ty = var_info.ty); 319 + } 320 + } 321 + suggestions 322 + } 323 + 324 + pub fn generate_suggestions( 325 + input: &str, 326 + context: Option<CompletionContext>, 327 + working_set: &StateWorkingSet, 328 + engine_guard: &EngineState, 329 + stack_guard: &Stack, 330 + root: &std::sync::Arc<vfs::VfsPath>, 331 + byte_pos: usize, 332 + ) -> Vec<Suggestion> { 333 + console_log!("context: {context:?}"); 334 + 335 + match context { 336 + Some(CompletionContext::Command { prefix, span }) => { 337 + generate_command_suggestions(input, working_set, prefix, span) 338 + } 339 + Some(CompletionContext::Argument { prefix, span }) => { 340 + generate_argument_suggestions(input, prefix, span, root) 341 + } 342 + Some(CompletionContext::Flag { 343 + prefix, 344 + span, 345 + command_name, 346 + }) => generate_flag_suggestions(input, engine_guard, prefix, span, command_name), 347 + Some(CompletionContext::CommandArgument { 348 + prefix, 349 + span, 350 + command_name, 351 + arg_index, 352 + }) => generate_command_argument_suggestions( 353 + input, 354 + engine_guard, 355 + prefix, 356 + span, 357 + command_name, 358 + arg_index, 359 + root, 360 + ), 361 + Some(CompletionContext::Variable { prefix, span }) => { 362 + generate_variable_suggestions(input, working_set, prefix, span, byte_pos) 363 + } 364 + Some(CompletionContext::CellPath { 365 + prefix, 366 + span, 367 + var_id, 368 + path_so_far, 369 + }) => generate_cell_path_suggestions( 370 + input, 371 + working_set, 372 + engine_guard, 373 + stack_guard, 374 + prefix, 375 + span, 376 + var_id, 377 + path_so_far, 378 + ), 379 + _ => { 380 + console_log!("[completion] Context is None, no suggestions generated"); 381 + Vec::new() 382 + } 383 + } 384 + }
+62
src/completion/types.rs
··· 1 + use nu_protocol::Span; 2 + use serde::Serialize; 3 + 4 + #[derive(Debug, Serialize)] 5 + pub struct Suggestion { 6 + pub name: String, 7 + pub description: Option<String>, 8 + pub is_command: bool, 9 + pub rendered: String, 10 + pub span_start: usize, // char index (not byte) 11 + pub span_end: usize, // char index (not byte) 12 + } 13 + 14 + impl PartialEq for Suggestion { 15 + fn eq(&self, other: &Self) -> bool { 16 + self.name == other.name 17 + } 18 + } 19 + impl Eq for Suggestion {} 20 + impl PartialOrd for Suggestion { 21 + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 22 + self.name.partial_cmp(&other.name) 23 + } 24 + } 25 + impl Ord for Suggestion { 26 + fn cmp(&self, other: &Self) -> std::cmp::Ordering { 27 + self.name.cmp(&other.name) 28 + } 29 + } 30 + 31 + #[derive(Debug)] 32 + pub enum CompletionContext { 33 + Command { 34 + prefix: String, 35 + span: Span, 36 + }, 37 + Argument { 38 + prefix: String, 39 + span: Span, 40 + }, 41 + Flag { 42 + prefix: String, 43 + span: Span, 44 + command_name: String, 45 + }, 46 + CommandArgument { 47 + prefix: String, 48 + span: Span, 49 + command_name: String, 50 + arg_index: usize, 51 + }, 52 + Variable { 53 + prefix: String, // without the $ prefix 54 + span: Span, 55 + }, 56 + CellPath { 57 + prefix: String, // the partial field name being typed (after the last dot) 58 + span: Span, // replacement span 59 + var_id: nu_protocol::VarId, // variable ID for evaluation 60 + path_so_far: Vec<String>, // path members accessed before current one 61 + }, 62 + }
+218
src/completion/variables.rs
··· 1 + use crate::console_log; 2 + use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; 3 + use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span, Value}; 4 + use std::collections::HashMap; 5 + 6 + pub fn eval_variable_for_completion( 7 + var_id: nu_protocol::VarId, 8 + working_set: &StateWorkingSet, 9 + engine_guard: &EngineState, 10 + stack_guard: &Stack, 11 + ) -> Option<Value> { 12 + match var_id { 13 + id if id == NU_VARIABLE_ID => { 14 + // $nu - get from engine state constant 15 + engine_guard.get_constant(id).cloned() 16 + } 17 + id if id == ENV_VARIABLE_ID => { 18 + // $env - build from environment variables in engine state 19 + // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars) 20 + let mut pairs: Vec<(String, Value)> = Vec::new(); 21 + for overlay_env in engine_guard.env_vars.values() { 22 + for (name, value) in overlay_env.iter() { 23 + pairs.push((name.clone(), value.clone())); 24 + } 25 + } 26 + pairs.sort_by(|a, b| a.0.cmp(&b.0)); 27 + // Deduplicate by name (later overlays override earlier ones) 28 + pairs.dedup_by(|a, b| a.0 == b.0); 29 + Some(Value::record(pairs.into_iter().collect(), Span::unknown())) 30 + } 31 + id if id == IN_VARIABLE_ID => { 32 + // $in - typically not available at completion time 33 + None 34 + } 35 + _ => { 36 + // User-defined variable - try to get const value first 37 + let var_info = working_set.get_variable(var_id); 38 + if let Some(const_val) = &var_info.const_val { 39 + Some(const_val.clone()) 40 + } else { 41 + // Variable doesn't have a const value (runtime value) 42 + // Try to get the value from the stack (runtime storage) 43 + match stack_guard.get_var(var_id, Span::unknown()) { 44 + Ok(value) => { 45 + console_log!("[completion] Found variable {var_id:?} value in stack"); 46 + Some(value) 47 + } 48 + Err(_) => { 49 + // Variable not in stack either 50 + console_log!( 51 + "[completion] Variable {var_id:?} has no const value and not in stack, type: {ty:?}", 52 + ty = var_info.ty 53 + ); 54 + None 55 + } 56 + } 57 + } 58 + } 59 + } 60 + } 61 + 62 + pub fn get_columns_from_value(value: &Value) -> Vec<(String, Option<String>)> { 63 + match value { 64 + Value::Record { val, .. } => val 65 + .iter() 66 + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 67 + .collect(), 68 + Value::List { vals, .. } => { 69 + // Get common columns from list of records 70 + if let Some(first) = vals.first() { 71 + if let Value::Record { val, .. } = first { 72 + return val 73 + .iter() 74 + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 75 + .collect(); 76 + } 77 + } 78 + vec![] 79 + } 80 + _ => vec![], 81 + } 82 + } 83 + 84 + pub fn follow_cell_path(value: &Value, path: &[&str]) -> Option<Value> { 85 + let mut current = value.clone(); 86 + for member in path { 87 + match &current { 88 + Value::Record { val, .. } => { 89 + current = val.get(member)?.clone(); 90 + } 91 + Value::List { vals, .. } => { 92 + // Try to parse as index or get from first record 93 + if let Ok(idx) = member.parse::<usize>() { 94 + current = vals.get(idx)?.clone(); 95 + } else if let Some(first) = vals.first() { 96 + if let Value::Record { val, .. } = first { 97 + current = val.get(member)?.clone(); 98 + } else { 99 + return None; 100 + } 101 + } else { 102 + return None; 103 + } 104 + } 105 + _ => return None, 106 + } 107 + } 108 + Some(current) 109 + } 110 + 111 + pub fn extract_closure_params(input: &str, cursor_pos: usize) -> Vec<String> { 112 + let mut params = Vec::new(); 113 + 114 + // Find all closures in the input by looking for {|...| patterns 115 + // We need to find closures that contain the cursor position 116 + let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions 117 + let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params) 118 + 119 + let mut i = 0; 120 + let chars: Vec<char> = input.chars().collect(); 121 + 122 + while i < chars.len() { 123 + if chars[i] == '{' { 124 + brace_stack.push(i); 125 + } else if chars[i] == '}' { 126 + if let Some(start) = brace_stack.pop() { 127 + // Check if this is a closure with parameters: {|param| ...} 128 + if start + 1 < chars.len() && chars[start + 1] == '|' { 129 + // Find the parameter list 130 + let param_start = start + 2; 131 + let mut param_end = param_start; 132 + 133 + // Find the closing | of the parameter list 134 + while param_end < chars.len() && chars[param_end] != '|' { 135 + param_end += 1; 136 + } 137 + 138 + if param_end < chars.len() { 139 + // Extract parameter names 140 + let params_text: String = chars[param_start..param_end].iter().collect(); 141 + let param_names: Vec<String> = params_text 142 + .split(',') 143 + .map(|s| s.trim().to_string()) 144 + .filter(|s| !s.is_empty()) 145 + .collect(); 146 + 147 + closures.push((start, i + 1, param_names)); 148 + } 149 + } 150 + } 151 + } 152 + i += 1; 153 + } 154 + 155 + // Find closures that contain the cursor position 156 + // A closure contains the cursor if: start <= cursor_pos < end 157 + for (start, end, param_names) in closures { 158 + if start <= cursor_pos && cursor_pos < end { 159 + console_log!( 160 + "[completion] Found closure at [{start}, {end}) containing cursor {cursor_pos}, params: {param_names:?}" 161 + ); 162 + params.extend(param_names); 163 + } 164 + } 165 + 166 + params 167 + } 168 + 169 + pub fn collect_variables( 170 + working_set: &StateWorkingSet, 171 + input: &str, 172 + cursor_pos: usize, 173 + ) -> HashMap<String, nu_protocol::VarId> { 174 + let mut variables = HashMap::new(); 175 + 176 + // Add built-in variables 177 + variables.insert("$nu".to_string(), NU_VARIABLE_ID); 178 + variables.insert("$in".to_string(), IN_VARIABLE_ID); 179 + variables.insert("$env".to_string(), ENV_VARIABLE_ID); 180 + 181 + // Collect closure parameters at cursor position 182 + // We don't need real var_ids for closure parameters since they're not evaluated yet 183 + // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder) 184 + // The actual var_id lookup will happen when the variable is used 185 + let closure_params = extract_closure_params(input, cursor_pos); 186 + for param_name in closure_params { 187 + let var_name = format!("${}", param_name); 188 + // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name 189 + // The completion logic only needs the name, not the actual var_id 190 + variables.insert(var_name.clone(), IN_VARIABLE_ID); 191 + console_log!("[completion] Added closure parameter: {var_name:?}"); 192 + } 193 + 194 + // Collect from working set delta scope 195 + let mut removed_overlays = vec![]; 196 + for scope_frame in working_set.delta.scope.iter().rev() { 197 + for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { 198 + for (name, var_id) in &overlay_frame.vars { 199 + let name = String::from_utf8_lossy(name).to_string(); 200 + variables.insert(name, *var_id); 201 + } 202 + } 203 + } 204 + 205 + // Collect from permanent state scope 206 + for overlay_frame in working_set 207 + .permanent_state 208 + .active_overlays(&removed_overlays) 209 + .rev() 210 + { 211 + for (name, var_id) in &overlay_frame.vars { 212 + let name = String::from_utf8_lossy(name).to_string(); 213 + variables.insert(name, *var_id); 214 + } 215 + } 216 + 217 + variables 218 + }
+1 -2
src/lib.rs
··· 7 7 use nu_cmd_extra::add_extra_command_context; 8 8 use nu_cmd_lang::create_default_context; 9 9 use nu_engine::{command_prelude::*, eval_block}; 10 - use nu_parser::{FlatShape, TokenContents, flatten_block, lex, parse}; 10 + use nu_parser::{FlatShape, flatten_block, parse}; 11 11 use nu_protocol::{ 12 12 Config, ListStream, PipelineData, Signals, Span, 13 13 engine::{EngineState, Stack, StateWorkingSet}, 14 14 }; 15 - use serde::Serialize; 16 15 use std::{ 17 16 io::Cursor, 18 17 sync::{Arc, OnceLock},