diff --git a/src/cmd/cd.rs b/src/cmd/cd.rs index 8d453ea..8b028b0 100644 --- a/src/cmd/cd.rs +++ b/src/cmd/cd.rs @@ -1,7 +1,7 @@ use crate::globals::{get_pwd, get_vfs, set_pwd, to_shell_err}; use nu_engine::CallExt; use nu_protocol::{ - Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, + Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type, engine::{Command, EngineState, Stack}, }; use std::sync::Arc; @@ -18,6 +18,7 @@ impl Command for Cd { fn signature(&self) -> Signature { Signature::build("cd") .optional("path", SyntaxShape::String, "the path to change into") + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::FileSystem) } diff --git a/src/cmd/fetch.rs b/src/cmd/fetch.rs index 9d15ab0..9eed8e2 100644 --- a/src/cmd/fetch.rs +++ b/src/cmd/fetch.rs @@ -20,11 +20,11 @@ use jacquard_repo::{ storage::{BlockStore, MemoryBlockStore}, }; use nu_engine::CallExt; -use nu_protocol::IntoPipelineData; use nu_protocol::{ Category, PipelineData, ShellError, Signature, SyntaxShape, Value, engine::{Command, EngineState, Stack}, }; +use nu_protocol::{IntoPipelineData, Type}; use std::io::Write; use std::str::FromStr; use std::sync::Arc; @@ -47,6 +47,7 @@ impl Command for Fetch { "HTTP URI or AT URI (at://identifier[/collection[/rkey]])", ) .named("output", SyntaxShape::Filepath, "output path", Some('o')) + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::Network) } diff --git a/src/cmd/job_kill.rs b/src/cmd/job_kill.rs index 6fca4a8..acb855b 100644 --- a/src/cmd/job_kill.rs +++ b/src/cmd/job_kill.rs @@ -1,7 +1,7 @@ use crate::globals::kill_task_by_id; use nu_engine::CallExt; use nu_protocol::{ - Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, + Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, engine::{Call, Command, EngineState, Stack}, }; @@ -16,6 +16,7 @@ impl Command for JobKill { fn signature(&self) -> Signature { Signature::build("job kill") .required("id", SyntaxShape::Int, "id of job to kill") + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::System) } diff --git a/src/cmd/job_list.rs b/src/cmd/job_list.rs index 38c9d8e..ecd8078 100644 --- a/src/cmd/job_list.rs +++ b/src/cmd/job_list.rs @@ -1,6 +1,6 @@ use crate::globals::get_all_tasks; use nu_protocol::{ - Category, ListStream, PipelineData, Record, ShellError, Signature, Value, + Category, ListStream, PipelineData, Record, ShellError, Signature, Type, Value, engine::{Call, Command, EngineState, Stack}, }; @@ -13,7 +13,9 @@ impl Command for JobList { } fn signature(&self) -> Signature { - Signature::build("job list").category(Category::System) + Signature::build("job list") + .input_output_type(Type::Nothing, Type::record()) + .category(Category::System) } fn description(&self) -> &str { diff --git a/src/cmd/ls.rs b/src/cmd/ls.rs index 96cc40c..6ae74b0 100644 --- a/src/cmd/ls.rs +++ b/src/cmd/ls.rs @@ -8,7 +8,7 @@ use crate::globals::{get_pwd, to_shell_err}; use jacquard::chrono; use nu_engine::CallExt; use nu_protocol::{ - Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Value, + Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value, engine::{Command, EngineState, Stack}, }; @@ -34,6 +34,7 @@ impl Command for Ls { Some('l'), ) .switch("full-paths", "display paths as absolute paths", Some('f')) + .input_output_type(Type::Nothing, Type::record()) .category(Category::FileSystem) } diff --git a/src/cmd/mkdir.rs b/src/cmd/mkdir.rs index ecd6f99..db4b7d6 100644 --- a/src/cmd/mkdir.rs +++ b/src/cmd/mkdir.rs @@ -1,7 +1,7 @@ use crate::globals::{get_pwd, to_shell_err}; use nu_engine::CallExt; use nu_protocol::{ - Category, PipelineData, ShellError, Signature, SyntaxShape, + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, engine::{Command, EngineState, Stack}, }; @@ -20,6 +20,7 @@ impl Command for Mkdir { SyntaxShape::String, "path of the directory(s) to create", ) + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::FileSystem) } diff --git a/src/cmd/open.rs b/src/cmd/open.rs index 3026bd6..1ad008b 100644 --- a/src/cmd/open.rs +++ b/src/cmd/open.rs @@ -4,7 +4,7 @@ use crate::globals::{get_pwd, to_shell_err}; use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml}; use nu_engine::CallExt; use nu_protocol::{ - ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape, + ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape, Type, engine::{Command, EngineState, Stack}, }; @@ -24,6 +24,7 @@ impl Command for Open { "output content as raw string/binary without parsing", Some('r'), ) + .input_output_type(Type::Nothing, Type::one_of([Type::String, Type::Binary])) .category(Category::FileSystem) } diff --git a/src/cmd/pwd.rs b/src/cmd/pwd.rs index 1d18715..8fdcfe8 100644 --- a/src/cmd/pwd.rs +++ b/src/cmd/pwd.rs @@ -1,4 +1,5 @@ use crate::globals::get_pwd; +use nu_protocol::Type; use nu_protocol::engine::Call; use nu_protocol::{ Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, @@ -14,7 +15,9 @@ impl Command for Pwd { } fn signature(&self) -> Signature { - Signature::build("pwd").category(Category::FileSystem) + Signature::build("pwd") + .input_output_type(Type::Nothing, Type::String) + .category(Category::FileSystem) } fn description(&self) -> &str { diff --git a/src/cmd/rm.rs b/src/cmd/rm.rs index eb42d65..9e64356 100644 --- a/src/cmd/rm.rs +++ b/src/cmd/rm.rs @@ -1,7 +1,7 @@ use crate::globals::{get_pwd, to_shell_err}; use nu_engine::CallExt; use nu_protocol::{ - Category, PipelineData, ShellError, Signature, SyntaxShape, + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, engine::{Command, EngineState, Stack}, }; use vfs::VfsFileType; @@ -26,6 +26,7 @@ impl Command for Rm { "remove directories and their contents recursively", Some('r'), ) + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::FileSystem) } diff --git a/src/cmd/source.rs b/src/cmd/source.rs index 70095f4..fae499f 100644 --- a/src/cmd/source.rs +++ b/src/cmd/source.rs @@ -2,7 +2,7 @@ use crate::globals::{get_pwd, queue_delta, to_shell_err}; use nu_engine::{CallExt, command_prelude::IoError, eval_block}; use nu_parser::parse; use nu_protocol::{ - Category, PipelineData, ShellError, Signature, SyntaxShape, + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, debugger::WithoutDebug, engine::{Command, EngineState, Stack, StateWorkingSet}, }; @@ -19,6 +19,7 @@ impl Command for Source { fn signature(&self) -> Signature { Signature::build(self.name()) .required("filename", SyntaxShape::String, "the file to source") + .input_output_type(Type::Nothing, Type::Nothing) .category(Category::Core) } diff --git a/src/cmd/sys.rs b/src/cmd/sys.rs index 0f094c7..62aafea 100644 --- a/src/cmd/sys.rs +++ b/src/cmd/sys.rs @@ -1,5 +1,6 @@ use js_sys::Reflect; use js_sys::global; +use nu_protocol::Type; use nu_protocol::{ Category, IntoPipelineData, PipelineData, Record, ShellError, Signature, Value, engine::{Command, EngineState, Stack}, @@ -15,7 +16,9 @@ impl Command for Sys { } fn signature(&self) -> Signature { - Signature::build("sys").category(Category::System) + Signature::build("sys") + .input_output_type(Type::Nothing, Type::record()) + .category(Category::System) } fn description(&self) -> &str { diff --git a/src/cmd/version.rs b/src/cmd/version.rs index 81e184f..4e8608c 100644 --- a/src/cmd/version.rs +++ b/src/cmd/version.rs @@ -1,3 +1,4 @@ +use nu_protocol::Type; use nu_protocol::engine::Call; use nu_protocol::{ Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, @@ -13,7 +14,9 @@ impl Command for Version { } fn signature(&self) -> Signature { - Signature::build(self.name()).category(Category::System) + Signature::build(self.name()) + .input_output_type(Type::Nothing, Type::String) + .category(Category::System) } fn description(&self) -> &str { diff --git a/src/completion.rs b/src/completion.rs index 331d8df..e89c80b 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,6 +1,8 @@ use futures::FutureExt; use js_sys::Promise; use wasm_bindgen_futures::future_to_promise; +use std::collections::HashMap; +use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Value}; use super::*; @@ -38,6 +40,7 @@ pub fn completion(input: String, js_cursor_pos: usize) -> Promise { pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { let engine_guard = read_engine_state().await; + let stack_guard = crate::read_stack().await; let root = get_pwd(); // Map UTF-16 cursor position (from JS) to Byte index (for Rust) @@ -253,18 +256,80 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { }) } else { web_sys::console::log_1(&JsValue::from_str(&format!( - "[completion] {} has no separator, checking for flag/argument context", + "[completion] {} has no separator, checking for variable/flag/argument context", shape_name ))); - // Check if this is a flag or command argument + // Check if this is a variable or cell path first let trimmed = trimmed_prefix.trim(); - let is_flag = trimmed.starts_with('-'); - - // Try to find the command and argument index - if let Some((cmd_name, arg_index)) = - find_command_and_arg_index(current_idx, local_span) - { - if is_flag { + + if trimmed.starts_with('$') { + // Variable or cell path completion + if let Some(dot_pos) = trimmed[1..].find('.') { + // Cell path completion: $in.name, $env.PWD, etc. + let var_name = &trimmed[1..dot_pos + 1]; + let after_var = &trimmed[dot_pos + 2..]; + let parts: Vec<&str> = after_var.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if after_var.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let var_id = match var_name { + "env" => Some(ENV_VARIABLE_ID), + "nu" => Some(NU_VARIABLE_ID), + "in" => Some(IN_VARIABLE_ID), + _ => working_set.find_variable(var_name.as_bytes()) + }; + + if let Some(var_id) = var_id { + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] {}: Setting CellPath context with var {:?}, prefix {:?}", + shape_name, var_name, cell_prefix + ))); + Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, adjusted_span.end), + var_id, + path_so_far, + }) + } else { + // Unknown variable, fall back to variable completion + let var_prefix = trimmed[1..].to_string(); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] {}: Unknown var, setting Variable context with prefix {:?}", + shape_name, var_prefix + ))); + Some(CompletionContext::Variable { + prefix: var_prefix, + span: adjusted_span, + }) + } + } else { + // Simple variable completion (no dot) + let var_prefix = if trimmed.len() > 1 { + trimmed[1..].to_string() + } else { + String::new() + }; + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] {}: Setting Variable context with prefix {:?}", + shape_name, var_prefix + ))); + Some(CompletionContext::Variable { + prefix: var_prefix, + span: adjusted_span, + }) + } + } else if trimmed.starts_with('-') { + // Flag completion + if let Some((cmd_name, _)) = find_command_and_arg_index(current_idx, local_span) { web_sys::console::log_1(&JsValue::from_str(&format!( "[completion] {}: Found command {:?} for flag completion", shape_name, cmd_name @@ -275,6 +340,16 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { command_name: cmd_name, }) } else { + Some(CompletionContext::Argument { + prefix: trimmed_prefix, + span: adjusted_span, + }) + } + } else { + // Try to find the command and argument index + if let Some((cmd_name, arg_index)) = + find_command_and_arg_index(current_idx, local_span) + { web_sys::console::log_1(&JsValue::from_str(&format!( "[completion] {}: Found command {:?} with arg_index {} for argument completion", shape_name, cmd_name, arg_index @@ -285,18 +360,17 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { command_name: cmd_name, arg_index, }) + } else { + // No command found, treat as regular argument + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] {}: No command found, using Argument context", + shape_name + ))); + Some(CompletionContext::Argument { + prefix: trimmed_prefix, + span: adjusted_span, + }) } - } else { - // No command found, treat as regular argument - web_sys::console::log_1(&JsValue::from_str(&format!( - "[completion] {}: No command found{}, using Argument context", - shape_name, - if is_flag { " for flag" } else { "" } - ))); - Some(CompletionContext::Argument { - prefix: trimmed_prefix, - span: adjusted_span, - }) } } } else { @@ -304,6 +378,225 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { } }; + // Helper function to evaluate a variable for completion + // Returns the Value of a variable if it can be evaluated + let eval_variable_for_completion = |var_id: nu_protocol::VarId, working_set: &StateWorkingSet| -> Option { + match var_id { + id if id == NU_VARIABLE_ID => { + // $nu - get from engine state constant + engine_guard.get_constant(id).cloned() + } + id if id == ENV_VARIABLE_ID => { + // $env - build from environment variables in engine state + // EnvVars is HashMap> (overlay -> vars) + let mut pairs: Vec<(String, Value)> = Vec::new(); + for overlay_env in engine_guard.env_vars.values() { + for (name, value) in overlay_env.iter() { + pairs.push((name.clone(), value.clone())); + } + } + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + // Deduplicate by name (later overlays override earlier ones) + pairs.dedup_by(|a, b| a.0 == b.0); + Some(Value::record(pairs.into_iter().collect(), Span::unknown())) + } + id if id == IN_VARIABLE_ID => { + // $in - typically not available at completion time + None + } + _ => { + // User-defined variable - try to get const value first + let var_info = working_set.get_variable(var_id); + if let Some(const_val) = &var_info.const_val { + Some(const_val.clone()) + } else { + // Variable doesn't have a const value (runtime value) + // Try to get the value from the stack (runtime storage) + match stack_guard.get_var(var_id, Span::unknown()) { + Ok(value) => { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Found variable {:?} value in stack", + var_id + ))); + Some(value) + } + Err(_) => { + // Variable not in stack either + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Variable {:?} has no const value and not in stack, type: {:?}", + var_id, var_info.ty + ))); + None + } + } + } + } + } + }; + + // Helper function to extract column/field names from a Value + let get_columns_from_value = |value: &Value| -> Vec<(String, Option)> { + match value { + Value::Record { val, .. } => { + val.iter() + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) + .collect() + } + Value::List { vals, .. } => { + // Get common columns from list of records + if let Some(first) = vals.first() { + if let Value::Record { val, .. } = first { + return val.iter() + .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) + .collect(); + } + } + vec![] + } + _ => vec![], + } + }; + + // Helper function to follow a cell path and get the value at that path + let follow_cell_path = |value: &Value, path: &[String]| -> Option { + let mut current = value.clone(); + for member in path { + match ¤t { + Value::Record { val, .. } => { + current = val.get(member)?.clone(); + } + Value::List { vals, .. } => { + // Try to parse as index or get from first record + if let Ok(idx) = member.parse::() { + current = vals.get(idx)?.clone(); + } else if let Some(first) = vals.first() { + if let Value::Record { val, .. } = first { + current = val.get(member)?.clone(); + } else { + return None; + } + } else { + return None; + } + } + _ => return None, + } + } + Some(current) + }; + + // Helper function to extract closure parameters from input string at cursor position + // We parse the input directly to find closures containing the cursor and extract their parameters + let extract_closure_params = |input: &str, cursor_pos: usize| -> Vec { + let mut params = Vec::new(); + + // Find all closures in the input by looking for {|...| patterns + // We need to find closures that contain the cursor position + let mut brace_stack: Vec = Vec::new(); // Stack of opening brace positions + let mut closures: Vec<(usize, usize, Vec)> = Vec::new(); // (start, end, params) + + let mut i = 0; + let chars: Vec = input.chars().collect(); + + while i < chars.len() { + if chars[i] == '{' { + brace_stack.push(i); + } else if chars[i] == '}' { + if let Some(start) = brace_stack.pop() { + // Check if this is a closure with parameters: {|param| ...} + if start + 1 < chars.len() && chars[start + 1] == '|' { + // Find the parameter list + let param_start = start + 2; + let mut param_end = param_start; + + // Find the closing | of the parameter list + while param_end < chars.len() && chars[param_end] != '|' { + param_end += 1; + } + + if param_end < chars.len() { + // Extract parameter names + let params_text: String = chars[param_start..param_end].iter().collect(); + let param_names: Vec = params_text + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + closures.push((start, i + 1, param_names)); + } + } + } + } + i += 1; + } + + // Find closures that contain the cursor position + // A closure contains the cursor if: start <= cursor_pos < end + for (start, end, param_names) in closures { + if start <= cursor_pos && cursor_pos < end { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Found closure at [{}, {}) containing cursor {}, params: {:?}", + start, end, cursor_pos, param_names + ))); + params.extend(param_names); + } + } + + params + }; + + // Helper function to collect variables from working set + let collect_variables = |working_set: &StateWorkingSet, input: &str, cursor_pos: usize| -> HashMap { + let mut variables = HashMap::new(); + + // Add built-in variables + variables.insert("$nu".to_string(), NU_VARIABLE_ID); + variables.insert("$in".to_string(), IN_VARIABLE_ID); + variables.insert("$env".to_string(), ENV_VARIABLE_ID); + + // Collect closure parameters at cursor position + // We don't need real var_ids for closure parameters since they're not evaluated yet + // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder) + // The actual var_id lookup will happen when the variable is used + let closure_params = extract_closure_params(input, cursor_pos); + for param_name in closure_params { + let var_name = format!("${}", param_name); + // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name + // The completion logic only needs the name, not the actual var_id + variables.insert(var_name.clone(), IN_VARIABLE_ID); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Added closure parameter: {:?}", + var_name + ))); + } + + // Collect from working set delta scope + let mut removed_overlays = vec![]; + for scope_frame in working_set.delta.scope.iter().rev() { + for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { + for (name, var_id) in &overlay_frame.vars { + let name = String::from_utf8_lossy(name).to_string(); + variables.insert(name, *var_id); + } + } + } + + // Collect from permanent state scope + for overlay_frame in working_set + .permanent_state + .active_overlays(&removed_overlays) + .rev() + { + for (name, var_id) in &overlay_frame.vars { + let name = String::from_utf8_lossy(name).to_string(); + variables.insert(name, *var_id); + } + } + + variables + }; + // Find what we're completing #[derive(Debug)] enum CompletionContext { @@ -326,6 +619,16 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { command_name: String, arg_index: usize, }, + Variable { + prefix: String, // without the $ prefix + span: Span, + }, + CellPath { + prefix: String, // the partial field name being typed (after the last dot) + span: Span, // replacement span + var_id: nu_protocol::VarId, // variable ID for evaluation + path_so_far: Vec, // path members accessed before current one + }, } let mut context: Option = None; @@ -416,6 +719,171 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { } } else { match shape { + // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes + // e.g., `$a.na` where $a is a Variable shape and `.na` is a String shape + _ if { + idx > 0 && matches!(shape, FlatShape::String) + } => { + // Look at the previous shape to see if it's a Variable + let prev_shape = &shapes[idx - 1]; + let prev_local_span = to_local_span(prev_shape.0); + + if let FlatShape::Variable(var_id) = prev_shape.1 { + // Check if the variable shape ends right where this shape starts (or very close) + // Allow for a small gap (like a dot) between shapes + let gap = local_span.start.saturating_sub(prev_local_span.end); + if gap <= 1 { + // This is a cell path - the String shape contains the field name(s) + // The prefix might be like "na" or "field.subfield" + let trimmed_prefix = prefix.trim(); + let parts: Vec<&str> = trimmed_prefix.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if trimmed_prefix.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = span.end.saturating_sub(prefix_byte_len); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Detected cell path from Variable+String shapes, var_id={:?}, prefix={:?}, path={:?}", + var_id, cell_prefix, path_so_far + ))); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, span.end), + var_id, + path_so_far, + }); + } else { + // Gap between shapes, fall through to default handling + context = Some(CompletionContext::Argument { prefix, span }); + } + } else { + // Previous shape is not a Variable, this is likely a regular string + context = Some(CompletionContext::Argument { prefix, span }); + } + } + // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes + // e.g., `{ $in. }` where $in is Shape 4 (Variable) and `. }` is Shape 5 (Block) + _ if { + let trimmed_prefix = prefix.trim(); + trimmed_prefix.starts_with('.') && idx > 0 + } => { + // Look at the previous shape to see if it's a Variable + let prev_shape = &shapes[idx - 1]; + let prev_local_span = to_local_span(prev_shape.0); + + if let FlatShape::Variable(var_id) = prev_shape.1 { + // Check if the variable shape ends right where this shape starts + if prev_local_span.end == local_span.start { + let trimmed_prefix = prefix.trim(); + // Parse path members from the prefix (which is like ".field" or ".field.subfield") + let after_dot = &trimmed_prefix[1..]; // Remove leading dot + let parts: Vec<&str> = after_dot.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() || (parts.len() == 1 && parts[0].is_empty()) { + (vec![], String::new()) + } else if after_dot.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = span.end.saturating_sub(prefix_byte_len); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Detected cell path from adjacent Variable shape, var_id={:?}, prefix={:?}", + var_id, cell_prefix + ))); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, span.end), + var_id, + path_so_far, + }); + } else { + // Gap between shapes, fall through to default handling + context = Some(CompletionContext::Argument { prefix, span }); + } + } else { + // Previous shape is not a Variable, this is likely a file path starting with . + context = Some(CompletionContext::Argument { prefix, span }); + } + } + _ if { + // Check if this is a variable or cell path (starts with $) before treating as command + let trimmed_prefix = prefix.trim(); + trimmed_prefix.starts_with('$') + } => { + let trimmed_prefix = prefix.trim(); + // Check if this is a cell path (contains a dot after $) + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { + // Cell path completion: $env.PWD, $nu.home-path, etc. + let var_name = &trimmed_prefix[1..dot_pos + 1]; // e.g., "env" + let after_var = &trimmed_prefix[dot_pos + 2..]; // e.g., "PWD" or "config.color" + + // Parse path members and current prefix + let parts: Vec<&str> = after_var.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if after_var.ends_with('.') { + // Cursor is right after a dot, complete all fields + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + // Cursor is in the middle of typing a field name + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + // Find the variable ID + let var_id = match var_name { + "env" => Some(ENV_VARIABLE_ID), + "nu" => Some(NU_VARIABLE_ID), + "in" => Some(IN_VARIABLE_ID), + _ => { + // Try to find user-defined variable + working_set.find_variable(var_name.as_bytes()) + } + }; + + if let Some(var_id) = var_id { + // Calculate span for the cell path member being completed + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = span.end.saturating_sub(prefix_byte_len); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, span.end), + var_id, + path_so_far, + }); + } else { + // Unknown variable, fall back to variable completion + let var_prefix = trimmed_prefix[1..].to_string(); + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span, + }); + } + } else { + // Variable completion context (no dot) + let var_prefix = if trimmed_prefix.len() > 1 { + trimmed_prefix[1..].to_string() + } else { + String::new() + }; + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span, + }); + } + } _ if is_command_shape(shape, local_span) => { let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix); context = Some(CompletionContext::Command { @@ -434,10 +902,102 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { context = Some(ctx); } } + FlatShape::Variable(var_id) => { + // Variable or cell path completion context + let trimmed_prefix = prefix.trim(); + if trimmed_prefix.starts_with('$') { + // Check if this is a cell path (contains a dot after $) + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { + // Cell path completion + let after_var = &trimmed_prefix[dot_pos + 2..]; + let parts: Vec<&str> = after_var.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if after_var.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = span.end.saturating_sub(prefix_byte_len); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, span.end), + var_id: *var_id, + path_so_far, + }); + } else { + // Simple variable completion + let var_prefix = trimmed_prefix[1..].to_string(); + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span, + }); + } + } else { + // Fallback to argument context if no $ found + context = Some(CompletionContext::Argument { prefix, span }); + } + } _ => { - // Check if this is a flag or command argument + // Check if this is a variable or cell path (starts with $) let trimmed_prefix = prefix.trim(); - if trimmed_prefix.starts_with('-') { + if trimmed_prefix.starts_with('$') { + // Check if this is a cell path (contains a dot after $) + if let Some(dot_pos) = trimmed_prefix[1..].find('.') { + // Cell path completion + let var_name = &trimmed_prefix[1..dot_pos + 1]; + let after_var = &trimmed_prefix[dot_pos + 2..]; + let parts: Vec<&str> = after_var.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if after_var.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let var_id = match var_name { + "env" => Some(ENV_VARIABLE_ID), + "nu" => Some(NU_VARIABLE_ID), + "in" => Some(IN_VARIABLE_ID), + _ => working_set.find_variable(var_name.as_bytes()) + }; + + if let Some(var_id) = var_id { + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = span.end.saturating_sub(prefix_byte_len); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, span.end), + var_id, + path_so_far, + }); + } else { + let var_prefix = trimmed_prefix[1..].to_string(); + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span, + }); + } + } else { + // Simple variable completion + let var_prefix = if trimmed_prefix.len() > 1 { + trimmed_prefix[1..].to_string() + } else { + String::new() + }; + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span, + }); + } + } else if trimmed_prefix.starts_with('-') { // This looks like a flag - find the command if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span) { @@ -573,9 +1133,72 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { last_word ))); } else { - // Check if this is a flag or command argument + // Check if this is a variable or cell path (starts with $) let trimmed_word = last_word.trim(); - if trimmed_word.starts_with('-') { + if trimmed_word.starts_with('$') { + // Check if this is a cell path (contains a dot after $) + if let Some(dot_pos) = trimmed_word[1..].find('.') { + // Cell path completion + let var_name = &trimmed_word[1..dot_pos + 1]; + let after_var = &trimmed_word[dot_pos + 2..]; + let parts: Vec<&str> = after_var.split('.').collect(); + let (path_so_far, cell_prefix) = if parts.is_empty() { + (vec![], String::new()) + } else if after_var.ends_with('.') { + (parts.iter().filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), String::new()) + } else { + let path: Vec = parts[..parts.len().saturating_sub(1)].iter().map(|s| s.to_string()).collect(); + let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); + (path, prefix) + }; + + let var_id = match var_name { + "env" => Some(ENV_VARIABLE_ID), + "nu" => Some(NU_VARIABLE_ID), + "in" => Some(IN_VARIABLE_ID), + _ => working_set.find_variable(var_name.as_bytes()) + }; + + if let Some(var_id) = var_id { + let prefix_byte_len = cell_prefix.len(); + let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); + let cell_prefix_clone = cell_prefix.clone(); + context = Some(CompletionContext::CellPath { + prefix: cell_prefix, + span: Span::new(cell_span_start, byte_pos), + var_id, + path_so_far, + }); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Set CellPath context with prefix: {:?}", + cell_prefix_clone + ))); + } else { + let var_prefix = trimmed_word[1..].to_string(); + let var_prefix_clone = var_prefix.clone(); + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span: Span::new(last_word_start, byte_pos), + }); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Set Variable context with prefix: {:?}", + var_prefix_clone + ))); + } + } else { + // Simple variable completion + let var_prefix = trimmed_word[1..].to_string(); + let var_prefix_clone = var_prefix.clone(); + context = Some(CompletionContext::Variable { + prefix: var_prefix, + span: Span::new(last_word_start, byte_pos), + }); + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Set Variable context with prefix: {:?}", + var_prefix_clone + ))); + } + } else if trimmed_word.starts_with('-') { // Try to find command by looking backwards through shapes let mut found_cmd = None; for (span, shape) in shapes.iter().rev() { @@ -1044,6 +1667,105 @@ pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { } } } + Some(CompletionContext::Variable { prefix, span }) => { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Generating Variable suggestions with prefix: {:?}", + prefix + ))); + + // Collect all available variables + let variables = collect_variables(&working_set, &input, byte_pos); + let span = to_char_span(span); + let mut var_count = 0; + + for (var_name, var_id) in variables { + // Filter by prefix (variable name includes $, so we need to check after $) + if var_name.len() > 1 && var_name[1..].starts_with(&prefix) { + // Get variable type + let var_type = working_set.get_variable(var_id).ty.to_string(); + + suggestions.push(Suggestion { + name: var_name.clone(), + description: Some(var_type.clone()), + is_command: false, + rendered: { + let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); + format!("{var_colored} {var_type}") + }, + span_start: span.start, + span_end: span.end, + }); + var_count += 1; + } + } + + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Found {} variable suggestions", + var_count + ))); + } + Some(CompletionContext::CellPath { prefix, span, var_id, path_so_far }) => { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Generating CellPath suggestions with prefix: {:?}, path: {:?}", + prefix, path_so_far + ))); + + // Evaluate the variable to get its value + if let Some(var_value) = eval_variable_for_completion(var_id, &working_set) { + // Follow the path to get the value at the current level + let current_value = if path_so_far.is_empty() { + var_value + } else { + follow_cell_path(&var_value, &path_so_far).unwrap_or(var_value) + }; + + // Get columns/fields from the current value + let columns = get_columns_from_value(¤t_value); + let span = to_char_span(span); + let mut field_count = 0; + + for (col_name, col_type) in columns { + // Filter by prefix + if col_name.starts_with(&prefix) { + let type_str = col_type.as_deref().unwrap_or("any"); + suggestions.push(Suggestion { + name: col_name.clone(), + description: Some(type_str.to_string()), + is_command: false, + rendered: { + let col_colored = ansi_term::Color::Yellow.paint(&col_name); + format!("{col_colored} {type_str}") + }, + span_start: span.start, + span_end: span.end, + }); + field_count += 1; + } + } + + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Found {} cell path suggestions", + field_count + ))); + } else { + // Variable couldn't be evaluated - this is expected for runtime variables + // We can't provide cell path completions without knowing the structure + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Could not evaluate variable {:?} for cell path completion (runtime variable)", + var_id + ))); + + // Try to get type information to provide better feedback + if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + working_set.get_variable(var_id) + })) { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[completion] Variable type: {:?}", + var_info.ty + ))); + } + } + } _ => { web_sys::console::log_1(&JsValue::from_str( "[completion] Context is None, no suggestions generated",