endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

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

implement variable and cell path completion

dawn 68d5c7d2 7ee40d91

+778 -37
+2 -1
src/cmd/cd.rs
··· 1 1 use crate::globals::{get_pwd, get_vfs, set_pwd, to_shell_err}; 2 2 use nu_engine::CallExt; 3 3 use nu_protocol::{ 4 - Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, 4 + Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type, 5 5 engine::{Command, EngineState, Stack}, 6 6 }; 7 7 use std::sync::Arc; ··· 18 18 fn signature(&self) -> Signature { 19 19 Signature::build("cd") 20 20 .optional("path", SyntaxShape::String, "the path to change into") 21 + .input_output_type(Type::Nothing, Type::Nothing) 21 22 .category(Category::FileSystem) 22 23 } 23 24
+2 -1
src/cmd/fetch.rs
··· 20 20 storage::{BlockStore, MemoryBlockStore}, 21 21 }; 22 22 use nu_engine::CallExt; 23 - use nu_protocol::IntoPipelineData; 24 23 use nu_protocol::{ 25 24 Category, PipelineData, ShellError, Signature, SyntaxShape, Value, 26 25 engine::{Command, EngineState, Stack}, 27 26 }; 27 + use nu_protocol::{IntoPipelineData, Type}; 28 28 use std::io::Write; 29 29 use std::str::FromStr; 30 30 use std::sync::Arc; ··· 47 47 "HTTP URI or AT URI (at://identifier[/collection[/rkey]])", 48 48 ) 49 49 .named("output", SyntaxShape::Filepath, "output path", Some('o')) 50 + .input_output_type(Type::Nothing, Type::Nothing) 50 51 .category(Category::Network) 51 52 } 52 53
+2 -1
src/cmd/job_kill.rs
··· 1 1 use crate::globals::kill_task_by_id; 2 2 use nu_engine::CallExt; 3 3 use nu_protocol::{ 4 - Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, 4 + Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 5 5 engine::{Call, Command, EngineState, Stack}, 6 6 }; 7 7 ··· 16 16 fn signature(&self) -> Signature { 17 17 Signature::build("job kill") 18 18 .required("id", SyntaxShape::Int, "id of job to kill") 19 + .input_output_type(Type::Nothing, Type::Nothing) 19 20 .category(Category::System) 20 21 } 21 22
+4 -2
src/cmd/job_list.rs
··· 1 1 use crate::globals::get_all_tasks; 2 2 use nu_protocol::{ 3 - Category, ListStream, PipelineData, Record, ShellError, Signature, Value, 3 + Category, ListStream, PipelineData, Record, ShellError, Signature, Type, Value, 4 4 engine::{Call, Command, EngineState, Stack}, 5 5 }; 6 6 ··· 13 13 } 14 14 15 15 fn signature(&self) -> Signature { 16 - Signature::build("job list").category(Category::System) 16 + Signature::build("job list") 17 + .input_output_type(Type::Nothing, Type::record()) 18 + .category(Category::System) 17 19 } 18 20 19 21 fn description(&self) -> &str {
+2 -1
src/cmd/ls.rs
··· 8 8 use jacquard::chrono; 9 9 use nu_engine::CallExt; 10 10 use nu_protocol::{ 11 - Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Value, 11 + Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value, 12 12 engine::{Command, EngineState, Stack}, 13 13 }; 14 14 ··· 34 34 Some('l'), 35 35 ) 36 36 .switch("full-paths", "display paths as absolute paths", Some('f')) 37 + .input_output_type(Type::Nothing, Type::record()) 37 38 .category(Category::FileSystem) 38 39 } 39 40
+2 -1
src/cmd/mkdir.rs
··· 1 1 use crate::globals::{get_pwd, to_shell_err}; 2 2 use nu_engine::CallExt; 3 3 use nu_protocol::{ 4 - Category, PipelineData, ShellError, Signature, SyntaxShape, 4 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 5 5 engine::{Command, EngineState, Stack}, 6 6 }; 7 7 ··· 20 20 SyntaxShape::String, 21 21 "path of the directory(s) to create", 22 22 ) 23 + .input_output_type(Type::Nothing, Type::Nothing) 23 24 .category(Category::FileSystem) 24 25 } 25 26
+2 -1
src/cmd/open.rs
··· 4 4 use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml}; 5 5 use nu_engine::CallExt; 6 6 use nu_protocol::{ 7 - ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape, 7 + ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 8 8 engine::{Command, EngineState, Stack}, 9 9 }; 10 10 ··· 24 24 "output content as raw string/binary without parsing", 25 25 Some('r'), 26 26 ) 27 + .input_output_type(Type::Nothing, Type::one_of([Type::String, Type::Binary])) 27 28 .category(Category::FileSystem) 28 29 } 29 30
+4 -1
src/cmd/pwd.rs
··· 1 1 use crate::globals::get_pwd; 2 + use nu_protocol::Type; 2 3 use nu_protocol::engine::Call; 3 4 use nu_protocol::{ 4 5 Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, ··· 14 15 } 15 16 16 17 fn signature(&self) -> Signature { 17 - Signature::build("pwd").category(Category::FileSystem) 18 + Signature::build("pwd") 19 + .input_output_type(Type::Nothing, Type::String) 20 + .category(Category::FileSystem) 18 21 } 19 22 20 23 fn description(&self) -> &str {
+2 -1
src/cmd/rm.rs
··· 1 1 use crate::globals::{get_pwd, to_shell_err}; 2 2 use nu_engine::CallExt; 3 3 use nu_protocol::{ 4 - Category, PipelineData, ShellError, Signature, SyntaxShape, 4 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 5 5 engine::{Command, EngineState, Stack}, 6 6 }; 7 7 use vfs::VfsFileType; ··· 26 26 "remove directories and their contents recursively", 27 27 Some('r'), 28 28 ) 29 + .input_output_type(Type::Nothing, Type::Nothing) 29 30 .category(Category::FileSystem) 30 31 } 31 32
+2 -1
src/cmd/source.rs
··· 2 2 use nu_engine::{CallExt, command_prelude::IoError, eval_block}; 3 3 use nu_parser::parse; 4 4 use nu_protocol::{ 5 - Category, PipelineData, ShellError, Signature, SyntaxShape, 5 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 6 6 debugger::WithoutDebug, 7 7 engine::{Command, EngineState, Stack, StateWorkingSet}, 8 8 }; ··· 19 19 fn signature(&self) -> Signature { 20 20 Signature::build(self.name()) 21 21 .required("filename", SyntaxShape::String, "the file to source") 22 + .input_output_type(Type::Nothing, Type::Nothing) 22 23 .category(Category::Core) 23 24 } 24 25
+4 -1
src/cmd/sys.rs
··· 1 1 use js_sys::Reflect; 2 2 use js_sys::global; 3 + use nu_protocol::Type; 3 4 use nu_protocol::{ 4 5 Category, IntoPipelineData, PipelineData, Record, ShellError, Signature, Value, 5 6 engine::{Command, EngineState, Stack}, ··· 15 16 } 16 17 17 18 fn signature(&self) -> Signature { 18 - Signature::build("sys").category(Category::System) 19 + Signature::build("sys") 20 + .input_output_type(Type::Nothing, Type::record()) 21 + .category(Category::System) 19 22 } 20 23 21 24 fn description(&self) -> &str {
+4 -1
src/cmd/version.rs
··· 1 + use nu_protocol::Type; 1 2 use nu_protocol::engine::Call; 2 3 use nu_protocol::{ 3 4 Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, ··· 13 14 } 14 15 15 16 fn signature(&self) -> Signature { 16 - Signature::build(self.name()).category(Category::System) 17 + Signature::build(self.name()) 18 + .input_output_type(Type::Nothing, Type::String) 19 + .category(Category::System) 17 20 } 18 21 19 22 fn description(&self) -> &str {
+746 -24
src/completion.rs
··· 1 1 use futures::FutureExt; 2 2 use js_sys::Promise; 3 3 use wasm_bindgen_futures::future_to_promise; 4 + use std::collections::HashMap; 5 + use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Value}; 4 6 5 7 use super::*; 6 8 ··· 38 40 39 41 pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { 40 42 let engine_guard = read_engine_state().await; 43 + let stack_guard = crate::read_stack().await; 41 44 let root = get_pwd(); 42 45 43 46 // Map UTF-16 cursor position (from JS) to Byte index (for Rust) ··· 253 256 }) 254 257 } else { 255 258 web_sys::console::log_1(&JsValue::from_str(&format!( 256 - "[completion] {} has no separator, checking for flag/argument context", 259 + "[completion] {} has no separator, checking for variable/flag/argument context", 257 260 shape_name 258 261 ))); 259 - // Check if this is a flag or command argument 262 + // Check if this is a variable or cell path first 260 263 let trimmed = trimmed_prefix.trim(); 261 - let is_flag = trimmed.starts_with('-'); 262 - 263 - // Try to find the command and argument index 264 - if let Some((cmd_name, arg_index)) = 265 - find_command_and_arg_index(current_idx, local_span) 266 - { 267 - if is_flag { 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 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 276 + } else { 277 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 278 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 279 + (path, prefix) 280 + }; 281 + 282 + let var_id = match var_name { 283 + "env" => Some(ENV_VARIABLE_ID), 284 + "nu" => Some(NU_VARIABLE_ID), 285 + "in" => Some(IN_VARIABLE_ID), 286 + _ => working_set.find_variable(var_name.as_bytes()) 287 + }; 288 + 289 + if let Some(var_id) = var_id { 290 + let prefix_byte_len = cell_prefix.len(); 291 + let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); 292 + web_sys::console::log_1(&JsValue::from_str(&format!( 293 + "[completion] {}: Setting CellPath context with var {:?}, prefix {:?}", 294 + shape_name, var_name, cell_prefix 295 + ))); 296 + Some(CompletionContext::CellPath { 297 + prefix: cell_prefix, 298 + span: Span::new(cell_span_start, adjusted_span.end), 299 + var_id, 300 + path_so_far, 301 + }) 302 + } else { 303 + // Unknown variable, fall back to variable completion 304 + let var_prefix = trimmed[1..].to_string(); 305 + web_sys::console::log_1(&JsValue::from_str(&format!( 306 + "[completion] {}: Unknown var, setting Variable context with prefix {:?}", 307 + shape_name, var_prefix 308 + ))); 309 + Some(CompletionContext::Variable { 310 + prefix: var_prefix, 311 + span: adjusted_span, 312 + }) 313 + } 314 + } else { 315 + // Simple variable completion (no dot) 316 + let var_prefix = if trimmed.len() > 1 { 317 + trimmed[1..].to_string() 318 + } else { 319 + String::new() 320 + }; 321 + web_sys::console::log_1(&JsValue::from_str(&format!( 322 + "[completion] {}: Setting Variable context with prefix {:?}", 323 + shape_name, var_prefix 324 + ))); 325 + Some(CompletionContext::Variable { 326 + prefix: var_prefix, 327 + span: adjusted_span, 328 + }) 329 + } 330 + } else if trimmed.starts_with('-') { 331 + // Flag completion 332 + if let Some((cmd_name, _)) = find_command_and_arg_index(current_idx, local_span) { 268 333 web_sys::console::log_1(&JsValue::from_str(&format!( 269 334 "[completion] {}: Found command {:?} for flag completion", 270 335 shape_name, cmd_name ··· 275 340 command_name: cmd_name, 276 341 }) 277 342 } else { 343 + Some(CompletionContext::Argument { 344 + prefix: trimmed_prefix, 345 + span: adjusted_span, 346 + }) 347 + } 348 + } else { 349 + // Try to find the command and argument index 350 + if let Some((cmd_name, arg_index)) = 351 + find_command_and_arg_index(current_idx, local_span) 352 + { 278 353 web_sys::console::log_1(&JsValue::from_str(&format!( 279 354 "[completion] {}: Found command {:?} with arg_index {} for argument completion", 280 355 shape_name, cmd_name, arg_index ··· 285 360 command_name: cmd_name, 286 361 arg_index, 287 362 }) 363 + } else { 364 + // No command found, treat as regular argument 365 + web_sys::console::log_1(&JsValue::from_str(&format!( 366 + "[completion] {}: No command found, using Argument context", 367 + shape_name 368 + ))); 369 + Some(CompletionContext::Argument { 370 + prefix: trimmed_prefix, 371 + span: adjusted_span, 372 + }) 288 373 } 289 - } else { 290 - // No command found, treat as regular argument 291 - web_sys::console::log_1(&JsValue::from_str(&format!( 292 - "[completion] {}: No command found{}, using Argument context", 293 - shape_name, 294 - if is_flag { " for flag" } else { "" } 295 - ))); 296 - Some(CompletionContext::Argument { 297 - prefix: trimmed_prefix, 298 - span: adjusted_span, 299 - }) 300 374 } 301 375 } 302 376 } else { ··· 304 378 } 305 379 }; 306 380 381 + // Helper function to evaluate a variable for completion 382 + // Returns the Value of a variable if it can be evaluated 383 + let eval_variable_for_completion = |var_id: nu_protocol::VarId, working_set: &StateWorkingSet| -> Option<Value> { 384 + match var_id { 385 + id if id == NU_VARIABLE_ID => { 386 + // $nu - get from engine state constant 387 + engine_guard.get_constant(id).cloned() 388 + } 389 + id if id == ENV_VARIABLE_ID => { 390 + // $env - build from environment variables in engine state 391 + // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars) 392 + let mut pairs: Vec<(String, Value)> = Vec::new(); 393 + for overlay_env in engine_guard.env_vars.values() { 394 + for (name, value) in overlay_env.iter() { 395 + pairs.push((name.clone(), value.clone())); 396 + } 397 + } 398 + pairs.sort_by(|a, b| a.0.cmp(&b.0)); 399 + // Deduplicate by name (later overlays override earlier ones) 400 + pairs.dedup_by(|a, b| a.0 == b.0); 401 + Some(Value::record(pairs.into_iter().collect(), Span::unknown())) 402 + } 403 + id if id == IN_VARIABLE_ID => { 404 + // $in - typically not available at completion time 405 + None 406 + } 407 + _ => { 408 + // User-defined variable - try to get const value first 409 + let var_info = working_set.get_variable(var_id); 410 + if let Some(const_val) = &var_info.const_val { 411 + Some(const_val.clone()) 412 + } else { 413 + // Variable doesn't have a const value (runtime value) 414 + // Try to get the value from the stack (runtime storage) 415 + match stack_guard.get_var(var_id, Span::unknown()) { 416 + Ok(value) => { 417 + web_sys::console::log_1(&JsValue::from_str(&format!( 418 + "[completion] Found variable {:?} value in stack", 419 + var_id 420 + ))); 421 + Some(value) 422 + } 423 + Err(_) => { 424 + // Variable not in stack either 425 + web_sys::console::log_1(&JsValue::from_str(&format!( 426 + "[completion] Variable {:?} has no const value and not in stack, type: {:?}", 427 + var_id, var_info.ty 428 + ))); 429 + None 430 + } 431 + } 432 + } 433 + } 434 + } 435 + }; 436 + 437 + // Helper function to extract column/field names from a Value 438 + let get_columns_from_value = |value: &Value| -> Vec<(String, Option<String>)> { 439 + match value { 440 + Value::Record { val, .. } => { 441 + val.iter() 442 + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 443 + .collect() 444 + } 445 + Value::List { vals, .. } => { 446 + // Get common columns from list of records 447 + if let Some(first) = vals.first() { 448 + if let Value::Record { val, .. } = first { 449 + return val.iter() 450 + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 451 + .collect(); 452 + } 453 + } 454 + vec![] 455 + } 456 + _ => vec![], 457 + } 458 + }; 459 + 460 + // Helper function to follow a cell path and get the value at that path 461 + let follow_cell_path = |value: &Value, path: &[String]| -> Option<Value> { 462 + let mut current = value.clone(); 463 + for member in path { 464 + match &current { 465 + Value::Record { val, .. } => { 466 + current = val.get(member)?.clone(); 467 + } 468 + Value::List { vals, .. } => { 469 + // Try to parse as index or get from first record 470 + if let Ok(idx) = member.parse::<usize>() { 471 + current = vals.get(idx)?.clone(); 472 + } else if let Some(first) = vals.first() { 473 + if let Value::Record { val, .. } = first { 474 + current = val.get(member)?.clone(); 475 + } else { 476 + return None; 477 + } 478 + } else { 479 + return None; 480 + } 481 + } 482 + _ => return None, 483 + } 484 + } 485 + Some(current) 486 + }; 487 + 488 + // Helper function to extract closure parameters from input string at cursor position 489 + // We parse the input directly to find closures containing the cursor and extract their parameters 490 + let extract_closure_params = |input: &str, cursor_pos: usize| -> Vec<String> { 491 + let mut params = Vec::new(); 492 + 493 + // Find all closures in the input by looking for {|...| patterns 494 + // We need to find closures that contain the cursor position 495 + let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions 496 + let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params) 497 + 498 + let mut i = 0; 499 + let chars: Vec<char> = input.chars().collect(); 500 + 501 + while i < chars.len() { 502 + if chars[i] == '{' { 503 + brace_stack.push(i); 504 + } else if chars[i] == '}' { 505 + if let Some(start) = brace_stack.pop() { 506 + // Check if this is a closure with parameters: {|param| ...} 507 + if start + 1 < chars.len() && chars[start + 1] == '|' { 508 + // Find the parameter list 509 + let param_start = start + 2; 510 + let mut param_end = param_start; 511 + 512 + // Find the closing | of the parameter list 513 + while param_end < chars.len() && chars[param_end] != '|' { 514 + param_end += 1; 515 + } 516 + 517 + if param_end < chars.len() { 518 + // Extract parameter names 519 + let params_text: String = chars[param_start..param_end].iter().collect(); 520 + let param_names: Vec<String> = params_text 521 + .split(',') 522 + .map(|s| s.trim().to_string()) 523 + .filter(|s| !s.is_empty()) 524 + .collect(); 525 + 526 + closures.push((start, i + 1, param_names)); 527 + } 528 + } 529 + } 530 + } 531 + i += 1; 532 + } 533 + 534 + // Find closures that contain the cursor position 535 + // A closure contains the cursor if: start <= cursor_pos < end 536 + for (start, end, param_names) in closures { 537 + if start <= cursor_pos && cursor_pos < end { 538 + web_sys::console::log_1(&JsValue::from_str(&format!( 539 + "[completion] Found closure at [{}, {}) containing cursor {}, params: {:?}", 540 + start, end, cursor_pos, param_names 541 + ))); 542 + params.extend(param_names); 543 + } 544 + } 545 + 546 + params 547 + }; 548 + 549 + // Helper function to collect variables from working set 550 + let collect_variables = |working_set: &StateWorkingSet, input: &str, cursor_pos: usize| -> HashMap<String, nu_protocol::VarId> { 551 + let mut variables = HashMap::new(); 552 + 553 + // Add built-in variables 554 + variables.insert("$nu".to_string(), NU_VARIABLE_ID); 555 + variables.insert("$in".to_string(), IN_VARIABLE_ID); 556 + variables.insert("$env".to_string(), ENV_VARIABLE_ID); 557 + 558 + // Collect closure parameters at cursor position 559 + // We don't need real var_ids for closure parameters since they're not evaluated yet 560 + // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder) 561 + // The actual var_id lookup will happen when the variable is used 562 + let closure_params = extract_closure_params(input, cursor_pos); 563 + for param_name in closure_params { 564 + let var_name = format!("${}", param_name); 565 + // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name 566 + // The completion logic only needs the name, not the actual var_id 567 + variables.insert(var_name.clone(), IN_VARIABLE_ID); 568 + web_sys::console::log_1(&JsValue::from_str(&format!( 569 + "[completion] Added closure parameter: {:?}", 570 + var_name 571 + ))); 572 + } 573 + 574 + // Collect from working set delta scope 575 + let mut removed_overlays = vec![]; 576 + for scope_frame in working_set.delta.scope.iter().rev() { 577 + for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { 578 + for (name, var_id) in &overlay_frame.vars { 579 + let name = String::from_utf8_lossy(name).to_string(); 580 + variables.insert(name, *var_id); 581 + } 582 + } 583 + } 584 + 585 + // Collect from permanent state scope 586 + for overlay_frame in working_set 587 + .permanent_state 588 + .active_overlays(&removed_overlays) 589 + .rev() 590 + { 591 + for (name, var_id) in &overlay_frame.vars { 592 + let name = String::from_utf8_lossy(name).to_string(); 593 + variables.insert(name, *var_id); 594 + } 595 + } 596 + 597 + variables 598 + }; 599 + 307 600 // Find what we're completing 308 601 #[derive(Debug)] 309 602 enum CompletionContext { ··· 326 619 command_name: String, 327 620 arg_index: usize, 328 621 }, 622 + Variable { 623 + prefix: String, // without the $ prefix 624 + span: Span, 625 + }, 626 + CellPath { 627 + prefix: String, // the partial field name being typed (after the last dot) 628 + span: Span, // replacement span 629 + var_id: nu_protocol::VarId, // variable ID for evaluation 630 + path_so_far: Vec<String>, // path members accessed before current one 631 + }, 329 632 } 330 633 331 634 let mut context: Option<CompletionContext> = None; ··· 416 719 } 417 720 } else { 418 721 match shape { 722 + // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes 723 + // e.g., `$a.na` where $a is a Variable shape and `.na` is a String shape 724 + _ if { 725 + idx > 0 && matches!(shape, FlatShape::String) 726 + } => { 727 + // Look at the previous shape to see if it's a Variable 728 + let prev_shape = &shapes[idx - 1]; 729 + let prev_local_span = to_local_span(prev_shape.0); 730 + 731 + if let FlatShape::Variable(var_id) = prev_shape.1 { 732 + // Check if the variable shape ends right where this shape starts (or very close) 733 + // Allow for a small gap (like a dot) between shapes 734 + let gap = local_span.start.saturating_sub(prev_local_span.end); 735 + if gap <= 1 { 736 + // This is a cell path - the String shape contains the field name(s) 737 + // The prefix might be like "na" or "field.subfield" 738 + let trimmed_prefix = prefix.trim(); 739 + let parts: Vec<&str> = trimmed_prefix.split('.').collect(); 740 + let (path_so_far, cell_prefix) = if parts.is_empty() { 741 + (vec![], String::new()) 742 + } else if trimmed_prefix.ends_with('.') { 743 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 744 + } else { 745 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 746 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 747 + (path, prefix) 748 + }; 749 + 750 + let prefix_byte_len = cell_prefix.len(); 751 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 752 + web_sys::console::log_1(&JsValue::from_str(&format!( 753 + "[completion] Detected cell path from Variable+String shapes, var_id={:?}, prefix={:?}, path={:?}", 754 + var_id, cell_prefix, path_so_far 755 + ))); 756 + context = Some(CompletionContext::CellPath { 757 + prefix: cell_prefix, 758 + span: Span::new(cell_span_start, span.end), 759 + var_id, 760 + path_so_far, 761 + }); 762 + } else { 763 + // Gap between shapes, fall through to default handling 764 + context = Some(CompletionContext::Argument { prefix, span }); 765 + } 766 + } else { 767 + // Previous shape is not a Variable, this is likely a regular string 768 + context = Some(CompletionContext::Argument { prefix, span }); 769 + } 770 + } 771 + // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes 772 + // e.g., `{ $in. }` where $in is Shape 4 (Variable) and `. }` is Shape 5 (Block) 773 + _ if { 774 + let trimmed_prefix = prefix.trim(); 775 + trimmed_prefix.starts_with('.') && idx > 0 776 + } => { 777 + // Look at the previous shape to see if it's a Variable 778 + let prev_shape = &shapes[idx - 1]; 779 + let prev_local_span = to_local_span(prev_shape.0); 780 + 781 + if let FlatShape::Variable(var_id) = prev_shape.1 { 782 + // Check if the variable shape ends right where this shape starts 783 + if prev_local_span.end == local_span.start { 784 + let trimmed_prefix = prefix.trim(); 785 + // Parse path members from the prefix (which is like ".field" or ".field.subfield") 786 + let after_dot = &trimmed_prefix[1..]; // Remove leading dot 787 + let parts: Vec<&str> = after_dot.split('.').collect(); 788 + let (path_so_far, cell_prefix) = if parts.is_empty() || (parts.len() == 1 && parts[0].is_empty()) { 789 + (vec![], String::new()) 790 + } else if after_dot.ends_with('.') { 791 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 792 + } else { 793 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 794 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 795 + (path, prefix) 796 + }; 797 + 798 + let prefix_byte_len = cell_prefix.len(); 799 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 800 + web_sys::console::log_1(&JsValue::from_str(&format!( 801 + "[completion] Detected cell path from adjacent Variable shape, var_id={:?}, prefix={:?}", 802 + var_id, cell_prefix 803 + ))); 804 + context = Some(CompletionContext::CellPath { 805 + prefix: cell_prefix, 806 + span: Span::new(cell_span_start, span.end), 807 + var_id, 808 + path_so_far, 809 + }); 810 + } else { 811 + // Gap between shapes, fall through to default handling 812 + context = Some(CompletionContext::Argument { prefix, span }); 813 + } 814 + } else { 815 + // Previous shape is not a Variable, this is likely a file path starting with . 816 + context = Some(CompletionContext::Argument { prefix, span }); 817 + } 818 + } 819 + _ if { 820 + // Check if this is a variable or cell path (starts with $) before treating as command 821 + let trimmed_prefix = prefix.trim(); 822 + trimmed_prefix.starts_with('$') 823 + } => { 824 + let trimmed_prefix = prefix.trim(); 825 + // Check if this is a cell path (contains a dot after $) 826 + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 827 + // Cell path completion: $env.PWD, $nu.home-path, etc. 828 + let var_name = &trimmed_prefix[1..dot_pos + 1]; // e.g., "env" 829 + let after_var = &trimmed_prefix[dot_pos + 2..]; // e.g., "PWD" or "config.color" 830 + 831 + // Parse path members and current prefix 832 + let parts: Vec<&str> = after_var.split('.').collect(); 833 + let (path_so_far, cell_prefix) = if parts.is_empty() { 834 + (vec![], String::new()) 835 + } else if after_var.ends_with('.') { 836 + // Cursor is right after a dot, complete all fields 837 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 838 + } else { 839 + // Cursor is in the middle of typing a field name 840 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 841 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 842 + (path, prefix) 843 + }; 844 + 845 + // Find the variable ID 846 + let var_id = match var_name { 847 + "env" => Some(ENV_VARIABLE_ID), 848 + "nu" => Some(NU_VARIABLE_ID), 849 + "in" => Some(IN_VARIABLE_ID), 850 + _ => { 851 + // Try to find user-defined variable 852 + working_set.find_variable(var_name.as_bytes()) 853 + } 854 + }; 855 + 856 + if let Some(var_id) = var_id { 857 + // Calculate span for the cell path member being completed 858 + let prefix_byte_len = cell_prefix.len(); 859 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 860 + context = Some(CompletionContext::CellPath { 861 + prefix: cell_prefix, 862 + span: Span::new(cell_span_start, span.end), 863 + var_id, 864 + path_so_far, 865 + }); 866 + } else { 867 + // Unknown variable, fall back to variable completion 868 + let var_prefix = trimmed_prefix[1..].to_string(); 869 + context = Some(CompletionContext::Variable { 870 + prefix: var_prefix, 871 + span, 872 + }); 873 + } 874 + } else { 875 + // Variable completion context (no dot) 876 + let var_prefix = if trimmed_prefix.len() > 1 { 877 + trimmed_prefix[1..].to_string() 878 + } else { 879 + String::new() 880 + }; 881 + context = Some(CompletionContext::Variable { 882 + prefix: var_prefix, 883 + span, 884 + }); 885 + } 886 + } 419 887 _ if is_command_shape(shape, local_span) => { 420 888 let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix); 421 889 context = Some(CompletionContext::Command { ··· 434 902 context = Some(ctx); 435 903 } 436 904 } 905 + FlatShape::Variable(var_id) => { 906 + // Variable or cell path completion context 907 + let trimmed_prefix = prefix.trim(); 908 + if trimmed_prefix.starts_with('$') { 909 + // Check if this is a cell path (contains a dot after $) 910 + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 911 + // Cell path completion 912 + let after_var = &trimmed_prefix[dot_pos + 2..]; 913 + let parts: Vec<&str> = after_var.split('.').collect(); 914 + let (path_so_far, cell_prefix) = if parts.is_empty() { 915 + (vec![], String::new()) 916 + } else if after_var.ends_with('.') { 917 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 918 + } else { 919 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 920 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 921 + (path, prefix) 922 + }; 923 + 924 + let prefix_byte_len = cell_prefix.len(); 925 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 926 + context = Some(CompletionContext::CellPath { 927 + prefix: cell_prefix, 928 + span: Span::new(cell_span_start, span.end), 929 + var_id: *var_id, 930 + path_so_far, 931 + }); 932 + } else { 933 + // Simple variable completion 934 + let var_prefix = trimmed_prefix[1..].to_string(); 935 + context = Some(CompletionContext::Variable { 936 + prefix: var_prefix, 937 + span, 938 + }); 939 + } 940 + } else { 941 + // Fallback to argument context if no $ found 942 + context = Some(CompletionContext::Argument { prefix, span }); 943 + } 944 + } 437 945 _ => { 438 - // Check if this is a flag or command argument 946 + // Check if this is a variable or cell path (starts with $) 439 947 let trimmed_prefix = prefix.trim(); 440 - if trimmed_prefix.starts_with('-') { 948 + if trimmed_prefix.starts_with('$') { 949 + // Check if this is a cell path (contains a dot after $) 950 + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 951 + // Cell path completion 952 + let var_name = &trimmed_prefix[1..dot_pos + 1]; 953 + let after_var = &trimmed_prefix[dot_pos + 2..]; 954 + let parts: Vec<&str> = after_var.split('.').collect(); 955 + let (path_so_far, cell_prefix) = if parts.is_empty() { 956 + (vec![], String::new()) 957 + } else if after_var.ends_with('.') { 958 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 959 + } else { 960 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 961 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 962 + (path, prefix) 963 + }; 964 + 965 + let var_id = match var_name { 966 + "env" => Some(ENV_VARIABLE_ID), 967 + "nu" => Some(NU_VARIABLE_ID), 968 + "in" => Some(IN_VARIABLE_ID), 969 + _ => working_set.find_variable(var_name.as_bytes()) 970 + }; 971 + 972 + if let Some(var_id) = var_id { 973 + let prefix_byte_len = cell_prefix.len(); 974 + let cell_span_start = span.end.saturating_sub(prefix_byte_len); 975 + context = Some(CompletionContext::CellPath { 976 + prefix: cell_prefix, 977 + span: Span::new(cell_span_start, span.end), 978 + var_id, 979 + path_so_far, 980 + }); 981 + } else { 982 + let var_prefix = trimmed_prefix[1..].to_string(); 983 + context = Some(CompletionContext::Variable { 984 + prefix: var_prefix, 985 + span, 986 + }); 987 + } 988 + } else { 989 + // Simple variable completion 990 + let var_prefix = if trimmed_prefix.len() > 1 { 991 + trimmed_prefix[1..].to_string() 992 + } else { 993 + String::new() 994 + }; 995 + context = Some(CompletionContext::Variable { 996 + prefix: var_prefix, 997 + span, 998 + }); 999 + } 1000 + } else if trimmed_prefix.starts_with('-') { 441 1001 // This looks like a flag - find the command 442 1002 if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span) 443 1003 { ··· 573 1133 last_word 574 1134 ))); 575 1135 } else { 576 - // Check if this is a flag or command argument 1136 + // Check if this is a variable or cell path (starts with $) 577 1137 let trimmed_word = last_word.trim(); 578 - if trimmed_word.starts_with('-') { 1138 + if trimmed_word.starts_with('$') { 1139 + // Check if this is a cell path (contains a dot after $) 1140 + if let Some(dot_pos) = trimmed_word[1..].find('.') { 1141 + // Cell path completion 1142 + let var_name = &trimmed_word[1..dot_pos + 1]; 1143 + let after_var = &trimmed_word[dot_pos + 2..]; 1144 + let parts: Vec<&str> = after_var.split('.').collect(); 1145 + let (path_so_far, cell_prefix) = if parts.is_empty() { 1146 + (vec![], String::new()) 1147 + } else if after_var.ends_with('.') { 1148 + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) 1149 + } else { 1150 + let path: Vec<String> = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); 1151 + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 1152 + (path, prefix) 1153 + }; 1154 + 1155 + let var_id = match var_name { 1156 + "env" => Some(ENV_VARIABLE_ID), 1157 + "nu" => Some(NU_VARIABLE_ID), 1158 + "in" => Some(IN_VARIABLE_ID), 1159 + _ => working_set.find_variable(var_name.as_bytes()) 1160 + }; 1161 + 1162 + if let Some(var_id) = var_id { 1163 + let prefix_byte_len = cell_prefix.len(); 1164 + let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 1165 + let cell_prefix_clone = cell_prefix.clone(); 1166 + context = Some(CompletionContext::CellPath { 1167 + prefix: cell_prefix, 1168 + span: Span::new(cell_span_start, byte_pos), 1169 + var_id, 1170 + path_so_far, 1171 + }); 1172 + web_sys::console::log_1(&JsValue::from_str(&format!( 1173 + "[completion] Set CellPath context with prefix: {:?}", 1174 + cell_prefix_clone 1175 + ))); 1176 + } else { 1177 + let var_prefix = trimmed_word[1..].to_string(); 1178 + let var_prefix_clone = var_prefix.clone(); 1179 + context = Some(CompletionContext::Variable { 1180 + prefix: var_prefix, 1181 + span: Span::new(last_word_start, byte_pos), 1182 + }); 1183 + web_sys::console::log_1(&JsValue::from_str(&format!( 1184 + "[completion] Set Variable context with prefix: {:?}", 1185 + var_prefix_clone 1186 + ))); 1187 + } 1188 + } else { 1189 + // Simple variable completion 1190 + let var_prefix = trimmed_word[1..].to_string(); 1191 + let var_prefix_clone = var_prefix.clone(); 1192 + context = Some(CompletionContext::Variable { 1193 + prefix: var_prefix, 1194 + span: Span::new(last_word_start, byte_pos), 1195 + }); 1196 + web_sys::console::log_1(&JsValue::from_str(&format!( 1197 + "[completion] Set Variable context with prefix: {:?}", 1198 + var_prefix_clone 1199 + ))); 1200 + } 1201 + } else if trimmed_word.starts_with('-') { 579 1202 // Try to find command by looking backwards through shapes 580 1203 let mut found_cmd = None; 581 1204 for (span, shape) in shapes.iter().rev() { ··· 1041 1664 } 1042 1665 } 1043 1666 } 1667 + } 1668 + } 1669 + } 1670 + Some(CompletionContext::Variable { prefix, span }) => { 1671 + web_sys::console::log_1(&JsValue::from_str(&format!( 1672 + "[completion] Generating Variable suggestions with prefix: {:?}", 1673 + prefix 1674 + ))); 1675 + 1676 + // Collect all available variables 1677 + let variables = collect_variables(&working_set, &input, byte_pos); 1678 + let span = to_char_span(span); 1679 + let mut var_count = 0; 1680 + 1681 + for (var_name, var_id) in variables { 1682 + // Filter by prefix (variable name includes $, so we need to check after $) 1683 + if var_name.len() > 1 && var_name[1..].starts_with(&prefix) { 1684 + // Get variable type 1685 + let var_type = working_set.get_variable(var_id).ty.to_string(); 1686 + 1687 + suggestions.push(Suggestion { 1688 + name: var_name.clone(), 1689 + description: Some(var_type.clone()), 1690 + is_command: false, 1691 + rendered: { 1692 + let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); 1693 + format!("{var_colored} {var_type}") 1694 + }, 1695 + span_start: span.start, 1696 + span_end: span.end, 1697 + }); 1698 + var_count += 1; 1699 + } 1700 + } 1701 + 1702 + web_sys::console::log_1(&JsValue::from_str(&format!( 1703 + "[completion] Found {} variable suggestions", 1704 + var_count 1705 + ))); 1706 + } 1707 + Some(CompletionContext::CellPath { prefix, span, var_id, path_so_far }) => { 1708 + web_sys::console::log_1(&JsValue::from_str(&format!( 1709 + "[completion] Generating CellPath suggestions with prefix: {:?}, path: {:?}", 1710 + prefix, path_so_far 1711 + ))); 1712 + 1713 + // Evaluate the variable to get its value 1714 + if let Some(var_value) = eval_variable_for_completion(var_id, &working_set) { 1715 + // Follow the path to get the value at the current level 1716 + let current_value = if path_so_far.is_empty() { 1717 + var_value 1718 + } else { 1719 + follow_cell_path(&var_value, &path_so_far).unwrap_or(var_value) 1720 + }; 1721 + 1722 + // Get columns/fields from the current value 1723 + let columns = get_columns_from_value(&current_value); 1724 + let span = to_char_span(span); 1725 + let mut field_count = 0; 1726 + 1727 + for (col_name, col_type) in columns { 1728 + // Filter by prefix 1729 + if col_name.starts_with(&prefix) { 1730 + let type_str = col_type.as_deref().unwrap_or("any"); 1731 + suggestions.push(Suggestion { 1732 + name: col_name.clone(), 1733 + description: Some(type_str.to_string()), 1734 + is_command: false, 1735 + rendered: { 1736 + let col_colored = ansi_term::Color::Yellow.paint(&col_name); 1737 + format!("{col_colored} {type_str}") 1738 + }, 1739 + span_start: span.start, 1740 + span_end: span.end, 1741 + }); 1742 + field_count += 1; 1743 + } 1744 + } 1745 + 1746 + web_sys::console::log_1(&JsValue::from_str(&format!( 1747 + "[completion] Found {} cell path suggestions", 1748 + field_count 1749 + ))); 1750 + } else { 1751 + // Variable couldn't be evaluated - this is expected for runtime variables 1752 + // We can't provide cell path completions without knowing the structure 1753 + web_sys::console::log_1(&JsValue::from_str(&format!( 1754 + "[completion] Could not evaluate variable {:?} for cell path completion (runtime variable)", 1755 + var_id 1756 + ))); 1757 + 1758 + // Try to get type information to provide better feedback 1759 + if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 1760 + working_set.get_variable(var_id) 1761 + })) { 1762 + web_sys::console::log_1(&JsValue::from_str(&format!( 1763 + "[completion] Variable type: {:?}", 1764 + var_info.ty 1765 + ))); 1044 1766 } 1045 1767 } 1046 1768 }