···22222323 fn signature(&self) -> Signature {
2424 Signature::build("ls")
2525- .optional("path", SyntaxShape::String, "the path to list")
2525+ .optional("path", SyntaxShape::Filepath, "the path to list")
2626 .switch(
2727 "all",
2828 "include hidden paths (that start with a dot)",
+1-1
src/cmd/open.rs
···18181919 fn signature(&self) -> Signature {
2020 Signature::build("open")
2121- .required("path", SyntaxShape::String, "path to the file")
2121+ .required("path", SyntaxShape::Filepath, "path to the file")
2222 .switch(
2323 "raw",
2424 "output content as raw string/binary without parsing",
+1-1
src/cmd/save.rs
···16161717 fn signature(&self) -> Signature {
1818 Signature::build("save")
1919- .required("path", SyntaxShape::String, "path to write the data to")
1919+ .required("path", SyntaxShape::Filepath, "path to write the data to")
2020 .input_output_types(vec![(Type::Any, Type::Nothing)])
2121 .category(Category::FileSystem)
2222 }
-1984
src/completion.rs
···11-use futures::FutureExt;
22-use js_sys::Promise;
33-use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Value};
44-use std::collections::HashMap;
55-use wasm_bindgen_futures::future_to_promise;
66-77-use super::*;
88-99-#[derive(Debug, Serialize)]
1010-struct Suggestion {
1111- name: String,
1212- description: Option<String>,
1313- is_command: bool,
1414- rendered: String,
1515- span_start: usize, // char index (not byte)
1616- span_end: usize, // char index (not byte)
1717-}
1818-1919-impl PartialEq for Suggestion {
2020- fn eq(&self, other: &Self) -> bool {
2121- self.name == other.name
2222- }
2323-}
2424-impl Eq for Suggestion {}
2525-impl PartialOrd for Suggestion {
2626- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2727- self.name.partial_cmp(&other.name)
2828- }
2929-}
3030-impl Ord for Suggestion {
3131- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3232- self.name.cmp(&other.name)
3333- }
3434-}
3535-3636-#[wasm_bindgen]
3737-pub fn completion(input: String, js_cursor_pos: usize) -> Promise {
3838- future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s))))
3939-}
4040-4141-pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String {
4242- let engine_guard = read_engine_state().await;
4343- let stack_guard = crate::read_stack().await;
4444- let root = get_pwd();
4545-4646- // Map UTF-16 cursor position (from JS) to Byte index (for Rust)
4747- let byte_pos = input
4848- .char_indices()
4949- .map(|(i, _)| i)
5050- .nth(js_cursor_pos)
5151- .unwrap_or(input.len());
5252-5353- let (working_set, shapes, global_offset) = {
5454- let mut working_set = StateWorkingSet::new(&engine_guard);
5555- let global_offset = working_set.next_span_start();
5656- let block = parse(&mut working_set, None, input.as_bytes(), false);
5757- let shapes = flatten_block(&working_set, &block);
5858- (working_set, shapes, global_offset)
5959- };
6060-6161- // Initial state logging
6262- web_sys::console::log_1(&JsValue::from_str(&format!(
6363- "[completion] Input: {:?}, JS cursor: {}, byte cursor: {}",
6464- input, js_cursor_pos, byte_pos
6565- )));
6666- web_sys::console::log_1(&JsValue::from_str(&format!(
6767- "[completion] Found {} shapes, global_offset: {}",
6868- shapes.len(),
6969- global_offset
7070- )));
7171- for (idx, (span, shape)) in shapes.iter().enumerate() {
7272- let (local_start, local_end) = (
7373- span.start.saturating_sub(global_offset),
7474- span.end.saturating_sub(global_offset),
7575- );
7676- web_sys::console::log_1(&JsValue::from_str(&format!(
7777- "[completion] Shape {}: {:?} at [{}, {}] (local: [{}, {}])",
7878- idx, shape, span.start, span.end, local_start, local_end
7979- )));
8080- }
8181-8282- // Helper functions
8383- let is_separator_char = |c: char| -> bool { ['|', ';', '(', '{'].contains(&c) };
8484-8585- let is_command_separator_char = |c: char| -> bool { ['|', ';'].contains(&c) };
8686-8787- let has_separator_between = |start: usize, end: usize| -> bool {
8888- if start < end && start < input.len() {
8989- let text_between = &input[start..std::cmp::min(end, input.len())];
9090- text_between.chars().any(|c| is_separator_char(c))
9191- } else {
9292- false
9393- }
9494- };
9595-9696- let find_last_separator_pos = |text: &str| -> Option<usize> {
9797- text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1)
9898- };
9999-100100- let ends_with_separator = |text: &str| -> bool {
101101- let text = text.trim_end();
102102- text.ends_with('|') || text.ends_with(';')
103103- };
104104-105105- let to_local_span = |span: Span| -> Span {
106106- Span::new(
107107- span.start.saturating_sub(global_offset),
108108- span.end.saturating_sub(global_offset),
109109- )
110110- };
111111-112112- let safe_slice = |span: Span| -> String {
113113- (span.start < input.len())
114114- .then(|| {
115115- let safe_end = std::cmp::min(span.end, input.len());
116116- input[span.start..safe_end].to_string()
117117- })
118118- .unwrap_or_default()
119119- };
120120-121121- let is_command_shape = |shape: &FlatShape, local_span: Span| -> bool {
122122- matches!(
123123- shape,
124124- FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword
125125- ) || matches!(shape, FlatShape::Garbage) && {
126126- if local_span.start < input.len() {
127127- let prev_text = &safe_slice(local_span);
128128- !prev_text.trim().starts_with('-')
129129- } else {
130130- false
131131- }
132132- }
133133- };
134134-135135- let handle_block_prefix = |prefix: &str, span: Span| -> Option<(String, Span, bool)> {
136136- let mut block_prefix = prefix;
137137- let mut block_span_start = span.start;
138138-139139- // Remove leading '{' and whitespace
140140- if block_prefix.starts_with('{') {
141141- block_prefix = &block_prefix[1..];
142142- block_span_start += 1;
143143- }
144144- let trimmed_block_prefix = block_prefix.trim_start();
145145- if trimmed_block_prefix != block_prefix {
146146- // Adjust span start to skip whitespace
147147- block_span_start += block_prefix.len() - trimmed_block_prefix.len();
148148- }
149149-150150- let is_empty = trimmed_block_prefix.is_empty();
151151- Some((
152152- trimmed_block_prefix.to_string(),
153153- Span::new(block_span_start, span.end),
154154- is_empty,
155155- ))
156156- };
157157-158158- // Helper function to find command name and count arguments before cursor
159159- let find_command_and_arg_index =
160160- |current_idx: usize, current_local_span: Span| -> Option<(String, usize)> {
161161- let mut command_name: Option<String> = None;
162162- let mut arg_count = 0;
163163-164164- // Look backwards through shapes to find the command
165165- for i in (0..current_idx).rev() {
166166- if let Some((prev_span, prev_shape)) = shapes.get(i) {
167167- let prev_local_span = to_local_span(*prev_span);
168168-169169- // Check if there's a separator between this shape and the next one
170170- let next_shape_start = if i + 1 < shapes.len() {
171171- to_local_span(shapes[i + 1].0).start
172172- } else {
173173- current_local_span.start
174174- };
175175-176176- if has_separator_between(prev_local_span.end, next_shape_start) {
177177- break; // Stop at separator
178178- }
179179-180180- if is_command_shape(prev_shape, prev_local_span) {
181181- // Found the command
182182- let cmd_text = safe_slice(prev_local_span);
183183- // Extract just the command name (first word, no flags)
184184- let cmd_name = cmd_text
185185- .split_whitespace()
186186- .next()
187187- .unwrap_or(&cmd_text)
188188- .trim();
189189- command_name = Some(cmd_name.to_string());
190190- break;
191191- } else {
192192- // This is an argument - count it if it's not a flag
193193- let arg_text = safe_slice(prev_local_span);
194194- let trimmed_arg = arg_text.trim();
195195- // Don't count flags (starting with -) or empty arguments
196196- if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
197197- arg_count += 1;
198198- }
199199- }
200200- }
201201- }
202202-203203- command_name.map(|name| (name, arg_count))
204204- };
205205-206206- // Helper function to handle both Block and Closure shapes
207207- let handle_block_or_closure = |prefix: &str,
208208- span: Span,
209209- shape_name: &str,
210210- current_idx: usize,
211211- local_span: Span|
212212- -> Option<CompletionContext> {
213213- web_sys::console::log_1(&JsValue::from_str(&format!(
214214- "[completion] Processing {} shape with prefix: {:?}",
215215- shape_name, prefix
216216- )));
217217-218218- // Check if the content ends with a pipe or semicolon
219219- let prefix_ends_with_separator = ends_with_separator(prefix);
220220- let last_sep_pos_in_prefix = if prefix_ends_with_separator {
221221- find_last_separator_pos(prefix)
222222- } else {
223223- None
224224- };
225225- web_sys::console::log_1(&JsValue::from_str(&format!(
226226- "[completion] {}: prefix_ends_with_separator={}, last_sep_pos_in_prefix={:?}",
227227- shape_name, prefix_ends_with_separator, last_sep_pos_in_prefix
228228- )));
229229-230230- if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
231231- web_sys::console::log_1(&JsValue::from_str(&format!(
232232- "[completion] {}: trimmed_prefix={:?}, is_empty={}",
233233- shape_name, trimmed_prefix, is_empty
234234- )));
235235-236236- if is_empty {
237237- // Empty block/closure or just whitespace - command context
238238- web_sys::console::log_1(&JsValue::from_str(&format!(
239239- "[completion] {} is empty, setting Command context",
240240- shape_name
241241- )));
242242- Some(CompletionContext::Command {
243243- prefix: String::new(),
244244- span: adjusted_span,
245245- })
246246- } else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
247247- // After a separator - command context
248248- let after_sep = prefix[last_sep_pos..].trim_start();
249249- web_sys::console::log_1(&JsValue::from_str(&format!(
250250- "[completion] {} has separator at {}, after_sep={:?}, setting Command context",
251251- shape_name, last_sep_pos, after_sep
252252- )));
253253- Some(CompletionContext::Command {
254254- prefix: after_sep.to_string(),
255255- span: Span::new(span.start + last_sep_pos, span.end),
256256- })
257257- } else {
258258- web_sys::console::log_1(&JsValue::from_str(&format!(
259259- "[completion] {} has no separator, checking for variable/flag/argument context",
260260- shape_name
261261- )));
262262- // Check if this is a variable or cell path first
263263- let trimmed = trimmed_prefix.trim();
264264-265265- if trimmed.starts_with('$') {
266266- // Variable or cell path completion
267267- if let Some(dot_pos) = trimmed[1..].find('.') {
268268- // Cell path completion: $in.name, $env.PWD, etc.
269269- let var_name = &trimmed[1..dot_pos + 1];
270270- let after_var = &trimmed[dot_pos + 2..];
271271- let parts: Vec<&str> = after_var.split('.').collect();
272272- let (path_so_far, cell_prefix) = if parts.is_empty() {
273273- (vec![], String::new())
274274- } else if after_var.ends_with('.') {
275275- (
276276- parts
277277- .iter()
278278- .filter(|s| !s.is_empty())
279279- .map(|s| s.to_string())
280280- .collect(),
281281- String::new(),
282282- )
283283- } else {
284284- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
285285- .iter()
286286- .map(|s| s.to_string())
287287- .collect();
288288- let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default();
289289- (path, prefix)
290290- };
291291-292292- let var_id = match var_name {
293293- "env" => Some(ENV_VARIABLE_ID),
294294- "nu" => Some(NU_VARIABLE_ID),
295295- "in" => Some(IN_VARIABLE_ID),
296296- _ => working_set.find_variable(var_name.as_bytes()),
297297- };
298298-299299- if let Some(var_id) = var_id {
300300- let prefix_byte_len = cell_prefix.len();
301301- let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len);
302302- web_sys::console::log_1(&JsValue::from_str(&format!(
303303- "[completion] {}: Setting CellPath context with var {:?}, prefix {:?}",
304304- shape_name, var_name, cell_prefix
305305- )));
306306- Some(CompletionContext::CellPath {
307307- prefix: cell_prefix,
308308- span: Span::new(cell_span_start, adjusted_span.end),
309309- var_id,
310310- path_so_far,
311311- })
312312- } else {
313313- // Unknown variable, fall back to variable completion
314314- let var_prefix = trimmed[1..].to_string();
315315- web_sys::console::log_1(&JsValue::from_str(&format!(
316316- "[completion] {}: Unknown var, setting Variable context with prefix {:?}",
317317- shape_name, var_prefix
318318- )));
319319- Some(CompletionContext::Variable {
320320- prefix: var_prefix,
321321- span: adjusted_span,
322322- })
323323- }
324324- } else {
325325- // Simple variable completion (no dot)
326326- let var_prefix = if trimmed.len() > 1 {
327327- trimmed[1..].to_string()
328328- } else {
329329- String::new()
330330- };
331331- web_sys::console::log_1(&JsValue::from_str(&format!(
332332- "[completion] {}: Setting Variable context with prefix {:?}",
333333- shape_name, var_prefix
334334- )));
335335- Some(CompletionContext::Variable {
336336- prefix: var_prefix,
337337- span: adjusted_span,
338338- })
339339- }
340340- } else if trimmed.starts_with('-') {
341341- // Flag completion
342342- if let Some((cmd_name, _)) = find_command_and_arg_index(current_idx, local_span)
343343- {
344344- web_sys::console::log_1(&JsValue::from_str(&format!(
345345- "[completion] {}: Found command {:?} for flag completion",
346346- shape_name, cmd_name
347347- )));
348348- Some(CompletionContext::Flag {
349349- prefix: trimmed.to_string(),
350350- span: adjusted_span,
351351- command_name: cmd_name,
352352- })
353353- } else {
354354- Some(CompletionContext::Argument {
355355- prefix: trimmed_prefix,
356356- span: adjusted_span,
357357- })
358358- }
359359- } else {
360360- // Try to find the command and argument index
361361- if let Some((cmd_name, arg_index)) =
362362- find_command_and_arg_index(current_idx, local_span)
363363- {
364364- web_sys::console::log_1(&JsValue::from_str(&format!(
365365- "[completion] {}: Found command {:?} with arg_index {} for argument completion",
366366- shape_name, cmd_name, arg_index
367367- )));
368368- Some(CompletionContext::CommandArgument {
369369- prefix: trimmed.to_string(),
370370- span: adjusted_span,
371371- command_name: cmd_name,
372372- arg_index,
373373- })
374374- } else {
375375- // No command found, treat as regular argument
376376- web_sys::console::log_1(&JsValue::from_str(&format!(
377377- "[completion] {}: No command found, using Argument context",
378378- shape_name
379379- )));
380380- Some(CompletionContext::Argument {
381381- prefix: trimmed_prefix,
382382- span: adjusted_span,
383383- })
384384- }
385385- }
386386- }
387387- } else {
388388- None
389389- }
390390- };
391391-392392- // Helper function to evaluate a variable for completion
393393- // Returns the Value of a variable if it can be evaluated
394394- let eval_variable_for_completion = |var_id: nu_protocol::VarId,
395395- working_set: &StateWorkingSet|
396396- -> Option<Value> {
397397- match var_id {
398398- id if id == NU_VARIABLE_ID => {
399399- // $nu - get from engine state constant
400400- engine_guard.get_constant(id).cloned()
401401- }
402402- id if id == ENV_VARIABLE_ID => {
403403- // $env - build from environment variables in engine state
404404- // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars)
405405- let mut pairs: Vec<(String, Value)> = Vec::new();
406406- for overlay_env in engine_guard.env_vars.values() {
407407- for (name, value) in overlay_env.iter() {
408408- pairs.push((name.clone(), value.clone()));
409409- }
410410- }
411411- pairs.sort_by(|a, b| a.0.cmp(&b.0));
412412- // Deduplicate by name (later overlays override earlier ones)
413413- pairs.dedup_by(|a, b| a.0 == b.0);
414414- Some(Value::record(pairs.into_iter().collect(), Span::unknown()))
415415- }
416416- id if id == IN_VARIABLE_ID => {
417417- // $in - typically not available at completion time
418418- None
419419- }
420420- _ => {
421421- // User-defined variable - try to get const value first
422422- let var_info = working_set.get_variable(var_id);
423423- if let Some(const_val) = &var_info.const_val {
424424- Some(const_val.clone())
425425- } else {
426426- // Variable doesn't have a const value (runtime value)
427427- // Try to get the value from the stack (runtime storage)
428428- match stack_guard.get_var(var_id, Span::unknown()) {
429429- Ok(value) => {
430430- web_sys::console::log_1(&JsValue::from_str(&format!(
431431- "[completion] Found variable {:?} value in stack",
432432- var_id
433433- )));
434434- Some(value)
435435- }
436436- Err(_) => {
437437- // Variable not in stack either
438438- web_sys::console::log_1(&JsValue::from_str(&format!(
439439- "[completion] Variable {:?} has no const value and not in stack, type: {:?}",
440440- var_id, var_info.ty
441441- )));
442442- None
443443- }
444444- }
445445- }
446446- }
447447- }
448448- };
449449-450450- // Helper function to extract column/field names from a Value
451451- let get_columns_from_value = |value: &Value| -> Vec<(String, Option<String>)> {
452452- match value {
453453- Value::Record { val, .. } => val
454454- .iter()
455455- .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
456456- .collect(),
457457- Value::List { vals, .. } => {
458458- // Get common columns from list of records
459459- if let Some(first) = vals.first() {
460460- if let Value::Record { val, .. } = first {
461461- return val
462462- .iter()
463463- .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
464464- .collect();
465465- }
466466- }
467467- vec![]
468468- }
469469- _ => vec![],
470470- }
471471- };
472472-473473- // Helper function to follow a cell path and get the value at that path
474474- let follow_cell_path = |value: &Value, path: &[String]| -> Option<Value> {
475475- let mut current = value.clone();
476476- for member in path {
477477- match ¤t {
478478- Value::Record { val, .. } => {
479479- current = val.get(member)?.clone();
480480- }
481481- Value::List { vals, .. } => {
482482- // Try to parse as index or get from first record
483483- if let Ok(idx) = member.parse::<usize>() {
484484- current = vals.get(idx)?.clone();
485485- } else if let Some(first) = vals.first() {
486486- if let Value::Record { val, .. } = first {
487487- current = val.get(member)?.clone();
488488- } else {
489489- return None;
490490- }
491491- } else {
492492- return None;
493493- }
494494- }
495495- _ => return None,
496496- }
497497- }
498498- Some(current)
499499- };
500500-501501- // Helper function to extract closure parameters from input string at cursor position
502502- // We parse the input directly to find closures containing the cursor and extract their parameters
503503- let extract_closure_params = |input: &str, cursor_pos: usize| -> Vec<String> {
504504- let mut params = Vec::new();
505505-506506- // Find all closures in the input by looking for {|...| patterns
507507- // We need to find closures that contain the cursor position
508508- let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions
509509- let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params)
510510-511511- let mut i = 0;
512512- let chars: Vec<char> = input.chars().collect();
513513-514514- while i < chars.len() {
515515- if chars[i] == '{' {
516516- brace_stack.push(i);
517517- } else if chars[i] == '}' {
518518- if let Some(start) = brace_stack.pop() {
519519- // Check if this is a closure with parameters: {|param| ...}
520520- if start + 1 < chars.len() && chars[start + 1] == '|' {
521521- // Find the parameter list
522522- let param_start = start + 2;
523523- let mut param_end = param_start;
524524-525525- // Find the closing | of the parameter list
526526- while param_end < chars.len() && chars[param_end] != '|' {
527527- param_end += 1;
528528- }
529529-530530- if param_end < chars.len() {
531531- // Extract parameter names
532532- let params_text: String =
533533- chars[param_start..param_end].iter().collect();
534534- let param_names: Vec<String> = params_text
535535- .split(',')
536536- .map(|s| s.trim().to_string())
537537- .filter(|s| !s.is_empty())
538538- .collect();
539539-540540- closures.push((start, i + 1, param_names));
541541- }
542542- }
543543- }
544544- }
545545- i += 1;
546546- }
547547-548548- // Find closures that contain the cursor position
549549- // A closure contains the cursor if: start <= cursor_pos < end
550550- for (start, end, param_names) in closures {
551551- if start <= cursor_pos && cursor_pos < end {
552552- web_sys::console::log_1(&JsValue::from_str(&format!(
553553- "[completion] Found closure at [{}, {}) containing cursor {}, params: {:?}",
554554- start, end, cursor_pos, param_names
555555- )));
556556- params.extend(param_names);
557557- }
558558- }
559559-560560- params
561561- };
562562-563563- // Helper function to collect variables from working set
564564- let collect_variables = |working_set: &StateWorkingSet,
565565- input: &str,
566566- cursor_pos: usize|
567567- -> HashMap<String, nu_protocol::VarId> {
568568- let mut variables = HashMap::new();
569569-570570- // Add built-in variables
571571- variables.insert("$nu".to_string(), NU_VARIABLE_ID);
572572- variables.insert("$in".to_string(), IN_VARIABLE_ID);
573573- variables.insert("$env".to_string(), ENV_VARIABLE_ID);
574574-575575- // Collect closure parameters at cursor position
576576- // We don't need real var_ids for closure parameters since they're not evaluated yet
577577- // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder)
578578- // The actual var_id lookup will happen when the variable is used
579579- let closure_params = extract_closure_params(input, cursor_pos);
580580- for param_name in closure_params {
581581- let var_name = format!("${}", param_name);
582582- // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name
583583- // The completion logic only needs the name, not the actual var_id
584584- variables.insert(var_name.clone(), IN_VARIABLE_ID);
585585- web_sys::console::log_1(&JsValue::from_str(&format!(
586586- "[completion] Added closure parameter: {:?}",
587587- var_name
588588- )));
589589- }
590590-591591- // Collect from working set delta scope
592592- let mut removed_overlays = vec![];
593593- for scope_frame in working_set.delta.scope.iter().rev() {
594594- for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
595595- for (name, var_id) in &overlay_frame.vars {
596596- let name = String::from_utf8_lossy(name).to_string();
597597- variables.insert(name, *var_id);
598598- }
599599- }
600600- }
601601-602602- // Collect from permanent state scope
603603- for overlay_frame in working_set
604604- .permanent_state
605605- .active_overlays(&removed_overlays)
606606- .rev()
607607- {
608608- for (name, var_id) in &overlay_frame.vars {
609609- let name = String::from_utf8_lossy(name).to_string();
610610- variables.insert(name, *var_id);
611611- }
612612- }
613613-614614- variables
615615- };
616616-617617- // Find what we're completing
618618- #[derive(Debug)]
619619- enum CompletionContext {
620620- Command {
621621- prefix: String,
622622- span: Span,
623623- },
624624- Argument {
625625- prefix: String,
626626- span: Span,
627627- },
628628- Flag {
629629- prefix: String,
630630- span: Span,
631631- command_name: String,
632632- },
633633- CommandArgument {
634634- prefix: String,
635635- span: Span,
636636- command_name: String,
637637- arg_index: usize,
638638- },
639639- Variable {
640640- prefix: String, // without the $ prefix
641641- span: Span,
642642- },
643643- CellPath {
644644- prefix: String, // the partial field name being typed (after the last dot)
645645- span: Span, // replacement span
646646- var_id: nu_protocol::VarId, // variable ID for evaluation
647647- path_so_far: Vec<String>, // path members accessed before current one
648648- },
649649- }
650650-651651- let mut context: Option<CompletionContext> = None;
652652-653653- // Helper function to build full command prefix by looking backwards through shapes
654654- let build_command_prefix =
655655- |current_idx: usize, current_local_span: Span, current_prefix: &str| -> (String, Span) {
656656- let mut span_start = current_local_span.start;
657657-658658- // Look backwards through shapes to find previous command words
659659- for i in (0..current_idx).rev() {
660660- if let Some((prev_span, prev_shape)) = shapes.get(i) {
661661- let prev_local_span = to_local_span(*prev_span);
662662-663663- if is_command_shape(prev_shape, prev_local_span) {
664664- // Check if there's a separator between this shape and the next one
665665- let next_shape_start = if i + 1 < shapes.len() {
666666- to_local_span(shapes[i + 1].0).start
667667- } else {
668668- current_local_span.start
669669- };
670670-671671- // Check if there's a separator (pipe, semicolon, etc.) between shapes
672672- // Whitespace is fine, but separators indicate a new command
673673- if has_separator_between(prev_local_span.end, next_shape_start) {
674674- break; // Stop at separator
675675- }
676676-677677- // Update span start to include this command word
678678- span_start = prev_local_span.start;
679679- } else {
680680- // Not a command shape, stop looking backwards
681681- break;
682682- }
683683- }
684684- }
685685-686686- // Extract the full prefix from the input, preserving exact spacing
687687- let span_end = current_local_span.end;
688688- let full_prefix = if span_start < input.len() {
689689- safe_slice(Span::new(span_start, span_end))
690690- } else {
691691- current_prefix.to_string()
692692- };
693693-694694- (full_prefix, Span::new(span_start, span_end))
695695- };
696696-697697- // Helper function to get command signature (needed for context determination)
698698- let get_command_signature = |cmd_name: &str| -> Option<nu_protocol::Signature> {
699699- engine_guard
700700- .find_decl(cmd_name.as_bytes(), &[])
701701- .map(|id| engine_guard.get_decl(id).signature())
702702- };
703703-704704- // First, check if cursor is within a shape
705705- for (idx, (span, shape)) in shapes.iter().enumerate() {
706706- let local_span = to_local_span(*span);
707707-708708- if local_span.start <= byte_pos && byte_pos <= local_span.end {
709709- web_sys::console::log_1(&JsValue::from_str(&format!(
710710- "[completion] Cursor in shape {}: {:?} at {:?}",
711711- idx, shape, local_span
712712- )));
713713-714714- // Check if there's a pipe or semicolon between this shape's end and the cursor
715715- // If so, we're starting a new command and should ignore this shape
716716- let has_sep = has_separator_between(local_span.end, byte_pos);
717717- if has_sep {
718718- web_sys::console::log_1(&JsValue::from_str(&format!(
719719- "[completion] Separator found between shape end ({}) and cursor ({}), skipping shape",
720720- local_span.end, byte_pos
721721- )));
722722- // There's a separator, so we're starting a new command - skip this shape
723723- continue;
724724- }
725725-726726- let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
727727- let prefix = safe_slice(span);
728728- web_sys::console::log_1(&JsValue::from_str(&format!(
729729- "[completion] Processing shape {} with prefix: {:?}",
730730- idx, prefix
731731- )));
732732-733733- // Special case: if prefix is just '{' (possibly with whitespace),
734734- // we're at the start of a block and should complete commands
735735- let trimmed_prefix = prefix.trim();
736736- if trimmed_prefix == "{" {
737737- // We're right after '{' - command context
738738- if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
739739- context = Some(CompletionContext::Command {
740740- prefix: String::new(),
741741- span: adjusted_span,
742742- });
743743- }
744744- } else {
745745- match shape {
746746- // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes
747747- // e.g., `$a.na` where $a is a Variable shape and `.na` is a String shape
748748- _ if { idx > 0 && matches!(shape, FlatShape::String) } => {
749749- // Look at the previous shape to see if it's a Variable
750750- let prev_shape = &shapes[idx - 1];
751751- let prev_local_span = to_local_span(prev_shape.0);
752752-753753- if let FlatShape::Variable(var_id) = prev_shape.1 {
754754- // Check if the variable shape ends right where this shape starts (or very close)
755755- // Allow for a small gap (like a dot) between shapes
756756- let gap = local_span.start.saturating_sub(prev_local_span.end);
757757- if gap <= 1 {
758758- // This is a cell path - the String shape contains the field name(s)
759759- // The prefix might be like "na" or "field.subfield"
760760- let trimmed_prefix = prefix.trim();
761761- let parts: Vec<&str> = trimmed_prefix.split('.').collect();
762762- let (path_so_far, cell_prefix) = if parts.is_empty() {
763763- (vec![], String::new())
764764- } else if trimmed_prefix.ends_with('.') {
765765- (
766766- parts
767767- .iter()
768768- .filter(|s| !s.is_empty())
769769- .map(|s| s.to_string())
770770- .collect(),
771771- String::new(),
772772- )
773773- } else {
774774- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
775775- .iter()
776776- .map(|s| s.to_string())
777777- .collect();
778778- let prefix =
779779- parts.last().map(|s| s.to_string()).unwrap_or_default();
780780- (path, prefix)
781781- };
782782-783783- let prefix_byte_len = cell_prefix.len();
784784- let cell_span_start = span.end.saturating_sub(prefix_byte_len);
785785- web_sys::console::log_1(&JsValue::from_str(&format!(
786786- "[completion] Detected cell path from Variable+String shapes, var_id={:?}, prefix={:?}, path={:?}",
787787- var_id, cell_prefix, path_so_far
788788- )));
789789- context = Some(CompletionContext::CellPath {
790790- prefix: cell_prefix,
791791- span: Span::new(cell_span_start, span.end),
792792- var_id,
793793- path_so_far,
794794- });
795795- } else {
796796- // Gap between shapes, check if this is a flag
797797- let trimmed_prefix = prefix.trim();
798798- if trimmed_prefix.starts_with('-') {
799799- // This looks like a flag - find the command
800800- if let Some((cmd_name, _)) =
801801- find_command_and_arg_index(idx, local_span)
802802- {
803803- context = Some(CompletionContext::Flag {
804804- prefix: trimmed_prefix.to_string(),
805805- span,
806806- command_name: cmd_name,
807807- });
808808- } else {
809809- context =
810810- Some(CompletionContext::Argument { prefix, span });
811811- }
812812- } else {
813813- context = Some(CompletionContext::Argument { prefix, span });
814814- }
815815- }
816816- } else {
817817- // Previous shape is not a Variable, check if this is a flag
818818- let trimmed_prefix = prefix.trim();
819819- if trimmed_prefix.starts_with('-') {
820820- // This looks like a flag - find the command
821821- if let Some((cmd_name, _)) =
822822- find_command_and_arg_index(idx, local_span)
823823- {
824824- context = Some(CompletionContext::Flag {
825825- prefix: trimmed_prefix.to_string(),
826826- span,
827827- command_name: cmd_name,
828828- });
829829- } else {
830830- context = Some(CompletionContext::Argument { prefix, span });
831831- }
832832- } else {
833833- // This is likely a regular string argument
834834- context = Some(CompletionContext::Argument { prefix, span });
835835- }
836836- }
837837- }
838838- // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes
839839- // e.g., `{ $in. }` where $in is Shape 4 (Variable) and `. }` is Shape 5 (Block)
840840- _ if {
841841- let trimmed_prefix = prefix.trim();
842842- trimmed_prefix.starts_with('.') && idx > 0
843843- } =>
844844- {
845845- // Look at the previous shape to see if it's a Variable
846846- let prev_shape = &shapes[idx - 1];
847847- let prev_local_span = to_local_span(prev_shape.0);
848848-849849- if let FlatShape::Variable(var_id) = prev_shape.1 {
850850- // Check if the variable shape ends right where this shape starts
851851- if prev_local_span.end == local_span.start {
852852- let trimmed_prefix = prefix.trim();
853853- // Parse path members from the prefix (which is like ".field" or ".field.subfield")
854854- let after_dot = &trimmed_prefix[1..]; // Remove leading dot
855855- let parts: Vec<&str> = after_dot.split('.').collect();
856856- let (path_so_far, cell_prefix) = if parts.is_empty()
857857- || (parts.len() == 1 && parts[0].is_empty())
858858- {
859859- (vec![], String::new())
860860- } else if after_dot.ends_with('.') {
861861- (
862862- parts
863863- .iter()
864864- .filter(|s| !s.is_empty())
865865- .map(|s| s.to_string())
866866- .collect(),
867867- String::new(),
868868- )
869869- } else {
870870- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
871871- .iter()
872872- .map(|s| s.to_string())
873873- .collect();
874874- let prefix =
875875- parts.last().map(|s| s.to_string()).unwrap_or_default();
876876- (path, prefix)
877877- };
878878-879879- let prefix_byte_len = cell_prefix.len();
880880- let cell_span_start = span.end.saturating_sub(prefix_byte_len);
881881- web_sys::console::log_1(&JsValue::from_str(&format!(
882882- "[completion] Detected cell path from adjacent Variable shape, var_id={:?}, prefix={:?}",
883883- var_id, cell_prefix
884884- )));
885885- context = Some(CompletionContext::CellPath {
886886- prefix: cell_prefix,
887887- span: Span::new(cell_span_start, span.end),
888888- var_id,
889889- path_so_far,
890890- });
891891- } else {
892892- // Gap between shapes, fall through to default handling
893893- context = Some(CompletionContext::Argument { prefix, span });
894894- }
895895- } else {
896896- // Previous shape is not a Variable, this is likely a file path starting with .
897897- context = Some(CompletionContext::Argument { prefix, span });
898898- }
899899- }
900900- _ if {
901901- // Check if this is a variable or cell path (starts with $) before treating as command
902902- let trimmed_prefix = prefix.trim();
903903- trimmed_prefix.starts_with('$')
904904- } =>
905905- {
906906- let trimmed_prefix = prefix.trim();
907907- // Check if this is a cell path (contains a dot after $)
908908- if let Some(dot_pos) = trimmed_prefix[1..].find('.') {
909909- // Cell path completion: $env.PWD, $nu.home-path, etc.
910910- let var_name = &trimmed_prefix[1..dot_pos + 1]; // e.g., "env"
911911- let after_var = &trimmed_prefix[dot_pos + 2..]; // e.g., "PWD" or "config.color"
912912-913913- // Parse path members and current prefix
914914- let parts: Vec<&str> = after_var.split('.').collect();
915915- let (path_so_far, cell_prefix) = if parts.is_empty() {
916916- (vec![], String::new())
917917- } else if after_var.ends_with('.') {
918918- // Cursor is right after a dot, complete all fields
919919- (
920920- parts
921921- .iter()
922922- .filter(|s| !s.is_empty())
923923- .map(|s| s.to_string())
924924- .collect(),
925925- String::new(),
926926- )
927927- } else {
928928- // Cursor is in the middle of typing a field name
929929- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
930930- .iter()
931931- .map(|s| s.to_string())
932932- .collect();
933933- let prefix =
934934- parts.last().map(|s| s.to_string()).unwrap_or_default();
935935- (path, prefix)
936936- };
937937-938938- // Find the variable ID
939939- let var_id = match var_name {
940940- "env" => Some(ENV_VARIABLE_ID),
941941- "nu" => Some(NU_VARIABLE_ID),
942942- "in" => Some(IN_VARIABLE_ID),
943943- _ => {
944944- // Try to find user-defined variable
945945- working_set.find_variable(var_name.as_bytes())
946946- }
947947- };
948948-949949- if let Some(var_id) = var_id {
950950- // Calculate span for the cell path member being completed
951951- let prefix_byte_len = cell_prefix.len();
952952- let cell_span_start = span.end.saturating_sub(prefix_byte_len);
953953- context = Some(CompletionContext::CellPath {
954954- prefix: cell_prefix,
955955- span: Span::new(cell_span_start, span.end),
956956- var_id,
957957- path_so_far,
958958- });
959959- } else {
960960- // Unknown variable, fall back to variable completion
961961- let var_prefix = trimmed_prefix[1..].to_string();
962962- context = Some(CompletionContext::Variable {
963963- prefix: var_prefix,
964964- span,
965965- });
966966- }
967967- } else {
968968- // Variable completion context (no dot)
969969- let var_prefix = if trimmed_prefix.len() > 1 {
970970- trimmed_prefix[1..].to_string()
971971- } else {
972972- String::new()
973973- };
974974- context = Some(CompletionContext::Variable {
975975- prefix: var_prefix,
976976- span,
977977- });
978978- }
979979- }
980980- _ if is_command_shape(shape, local_span) => {
981981- let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix);
982982- context = Some(CompletionContext::Command {
983983- prefix: full_prefix,
984984- span: full_span,
985985- });
986986- }
987987- FlatShape::Block | FlatShape::Closure => {
988988- if let Some(ctx) = handle_block_or_closure(
989989- &prefix,
990990- span,
991991- shape.as_str().trim_start_matches("shape_"),
992992- idx,
993993- local_span,
994994- ) {
995995- context = Some(ctx);
996996- }
997997- }
998998- FlatShape::Variable(var_id) => {
999999- // Variable or cell path completion context
10001000- let trimmed_prefix = prefix.trim();
10011001- if trimmed_prefix.starts_with('$') {
10021002- // Check if this is a cell path (contains a dot after $)
10031003- if let Some(dot_pos) = trimmed_prefix[1..].find('.') {
10041004- // Cell path completion
10051005- let after_var = &trimmed_prefix[dot_pos + 2..];
10061006- let parts: Vec<&str> = after_var.split('.').collect();
10071007- let (path_so_far, cell_prefix) = if parts.is_empty() {
10081008- (vec![], String::new())
10091009- } else if after_var.ends_with('.') {
10101010- (
10111011- parts
10121012- .iter()
10131013- .filter(|s| !s.is_empty())
10141014- .map(|s| s.to_string())
10151015- .collect(),
10161016- String::new(),
10171017- )
10181018- } else {
10191019- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
10201020- .iter()
10211021- .map(|s| s.to_string())
10221022- .collect();
10231023- let prefix =
10241024- parts.last().map(|s| s.to_string()).unwrap_or_default();
10251025- (path, prefix)
10261026- };
10271027-10281028- let prefix_byte_len = cell_prefix.len();
10291029- let cell_span_start = span.end.saturating_sub(prefix_byte_len);
10301030- context = Some(CompletionContext::CellPath {
10311031- prefix: cell_prefix,
10321032- span: Span::new(cell_span_start, span.end),
10331033- var_id: *var_id,
10341034- path_so_far,
10351035- });
10361036- } else {
10371037- // Simple variable completion
10381038- let var_prefix = trimmed_prefix[1..].to_string();
10391039- context = Some(CompletionContext::Variable {
10401040- prefix: var_prefix,
10411041- span,
10421042- });
10431043- }
10441044- } else {
10451045- // Fallback to argument context if no $ found
10461046- context = Some(CompletionContext::Argument { prefix, span });
10471047- }
10481048- }
10491049- _ => {
10501050- // Check if this is a variable or cell path (starts with $)
10511051- let trimmed_prefix = prefix.trim();
10521052- if trimmed_prefix.starts_with('$') {
10531053- // Check if this is a cell path (contains a dot after $)
10541054- if let Some(dot_pos) = trimmed_prefix[1..].find('.') {
10551055- // Cell path completion
10561056- let var_name = &trimmed_prefix[1..dot_pos + 1];
10571057- let after_var = &trimmed_prefix[dot_pos + 2..];
10581058- let parts: Vec<&str> = after_var.split('.').collect();
10591059- let (path_so_far, cell_prefix) = if parts.is_empty() {
10601060- (vec![], String::new())
10611061- } else if after_var.ends_with('.') {
10621062- (
10631063- parts
10641064- .iter()
10651065- .filter(|s| !s.is_empty())
10661066- .map(|s| s.to_string())
10671067- .collect(),
10681068- String::new(),
10691069- )
10701070- } else {
10711071- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
10721072- .iter()
10731073- .map(|s| s.to_string())
10741074- .collect();
10751075- let prefix =
10761076- parts.last().map(|s| s.to_string()).unwrap_or_default();
10771077- (path, prefix)
10781078- };
10791079-10801080- let var_id = match var_name {
10811081- "env" => Some(ENV_VARIABLE_ID),
10821082- "nu" => Some(NU_VARIABLE_ID),
10831083- "in" => Some(IN_VARIABLE_ID),
10841084- _ => working_set.find_variable(var_name.as_bytes()),
10851085- };
10861086-10871087- if let Some(var_id) = var_id {
10881088- let prefix_byte_len = cell_prefix.len();
10891089- let cell_span_start = span.end.saturating_sub(prefix_byte_len);
10901090- context = Some(CompletionContext::CellPath {
10911091- prefix: cell_prefix,
10921092- span: Span::new(cell_span_start, span.end),
10931093- var_id,
10941094- path_so_far,
10951095- });
10961096- } else {
10971097- let var_prefix = trimmed_prefix[1..].to_string();
10981098- context = Some(CompletionContext::Variable {
10991099- prefix: var_prefix,
11001100- span,
11011101- });
11021102- }
11031103- } else {
11041104- // Simple variable completion
11051105- let var_prefix = if trimmed_prefix.len() > 1 {
11061106- trimmed_prefix[1..].to_string()
11071107- } else {
11081108- String::new()
11091109- };
11101110- context = Some(CompletionContext::Variable {
11111111- prefix: var_prefix,
11121112- span,
11131113- });
11141114- }
11151115- } else if trimmed_prefix.starts_with('-') {
11161116- // This looks like a flag - find the command
11171117- if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span)
11181118- {
11191119- context = Some(CompletionContext::Flag {
11201120- prefix: trimmed_prefix.to_string(),
11211121- span,
11221122- command_name: cmd_name,
11231123- });
11241124- } else {
11251125- context = Some(CompletionContext::Argument { prefix, span });
11261126- }
11271127- } else {
11281128- // This is a positional argument - find the command and argument index
11291129- if let Some((cmd_name, arg_index)) =
11301130- find_command_and_arg_index(idx, local_span)
11311131- {
11321132- context = Some(CompletionContext::CommandArgument {
11331133- prefix: trimmed_prefix.to_string(),
11341134- span,
11351135- command_name: cmd_name,
11361136- arg_index,
11371137- });
11381138- } else {
11391139- context = Some(CompletionContext::Argument { prefix, span });
11401140- }
11411141- }
11421142- }
11431143- }
11441144- }
11451145- break;
11461146- }
11471147- }
11481148-11491149- // If not in a shape, check what comes before the cursor
11501150- if context.is_none() {
11511151- web_sys::console::log_1(&JsValue::from_str(
11521152- "[completion] Context is None, entering fallback logic",
11531153- ));
11541154- // Check if there's a command-like shape before us
11551155- let mut found_command_before = false;
11561156- let mut has_separator_after_command = false;
11571157- for (span, shape) in shapes.iter().rev() {
11581158- let local_span = to_local_span(*span);
11591159- if local_span.end <= byte_pos {
11601160- if is_command_shape(shape, local_span) {
11611161- // Check if there's a pipe or semicolon between this command and the cursor
11621162- has_separator_after_command = has_separator_between(local_span.end, byte_pos);
11631163- web_sys::console::log_1(&JsValue::from_str(&format!(
11641164- "[completion] Found command shape {:?} at {:?}, has_separator_after_command={}",
11651165- shape, local_span, has_separator_after_command
11661166- )));
11671167- if !has_separator_after_command {
11681168- found_command_before = true;
11691169-11701170- // Extract the command text
11711171- let cmd = safe_slice(local_span);
11721172- let cmd_name = cmd.split_whitespace().next().unwrap_or(&cmd).trim();
11731173-11741174- // Check if we're right after the command (only whitespace between command and cursor)
11751175- let text_after_command = if local_span.end < input.len() {
11761176- &input[local_span.end..byte_pos]
11771177- } else {
11781178- ""
11791179- };
11801180- let is_right_after_command = text_after_command.trim().is_empty();
11811181-11821182- // If we're right after a command, check if it has positional arguments
11831183- if is_right_after_command {
11841184- if let Some(signature) = get_command_signature(cmd_name) {
11851185- // Check if command has any positional arguments
11861186- let has_positional_args = !signature.required_positional.is_empty()
11871187- || !signature.optional_positional.is_empty();
11881188-11891189- if has_positional_args {
11901190- // Count existing arguments before cursor
11911191- let mut arg_count = 0;
11921192- for (prev_span, prev_shape) in shapes.iter().rev() {
11931193- let prev_local_span = to_local_span(*prev_span);
11941194- if prev_local_span.end <= byte_pos
11951195- && prev_local_span.end > local_span.end
11961196- {
11971197- if !is_command_shape(prev_shape, prev_local_span) {
11981198- let arg_text = safe_slice(prev_local_span);
11991199- let trimmed_arg = arg_text.trim();
12001200- // Don't count flags (starting with -) or empty arguments
12011201- if !trimmed_arg.is_empty()
12021202- && !trimmed_arg.starts_with('-')
12031203- {
12041204- arg_count += 1;
12051205- }
12061206- }
12071207- }
12081208- }
12091209-12101210- web_sys::console::log_1(&JsValue::from_str(&format!(
12111211- "[completion] Right after command {:?}, setting CommandArgument context with arg_index: {}",
12121212- cmd_name, arg_count
12131213- )));
12141214-12151215- context = Some(CompletionContext::CommandArgument {
12161216- prefix: String::new(),
12171217- span: Span::new(byte_pos, byte_pos),
12181218- command_name: cmd_name.to_string(),
12191219- arg_index: arg_count,
12201220- });
12211221- } else {
12221222- // No positional arguments, don't show any completions
12231223- web_sys::console::log_1(&JsValue::from_str(&format!(
12241224- "[completion] Command {:?} has no positional args, not showing completions",
12251225- cmd_name
12261226- )));
12271227- // Leave context as None to show no completions
12281228- }
12291229- } else {
12301230- // Couldn't find signature, don't show completions
12311231- web_sys::console::log_1(&JsValue::from_str(&format!(
12321232- "[completion] Could not find signature for {:?}, not showing completions",
12331233- cmd_name
12341234- )));
12351235- // Leave context as None to show no completions
12361236- }
12371237- } else {
12381238- // Not right after command, complete the command itself
12391239- web_sys::console::log_1(&JsValue::from_str(&format!(
12401240- "[completion] Set Command context with prefix: {:?}",
12411241- cmd
12421242- )));
12431243- context = Some(CompletionContext::Command {
12441244- prefix: cmd,
12451245- span: local_span,
12461246- });
12471247- }
12481248- }
12491249- }
12501250- break;
12511251- }
12521252- }
12531253-12541254- if !found_command_before {
12551255- web_sys::console::log_1(&JsValue::from_str(
12561256- "[completion] No command found before cursor, checking tokens",
12571257- ));
12581258- // No command before, check context from tokens
12591259- let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
12601260- let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
12611261-12621262- let is_cmd_context = if let Some(token) = last_token {
12631263- let matches = matches!(
12641264- token.contents,
12651265- TokenContents::Pipe
12661266- | TokenContents::PipePipe
12671267- | TokenContents::Semicolon
12681268- | TokenContents::Eol
12691269- );
12701270- web_sys::console::log_1(&JsValue::from_str(&format!(
12711271- "[completion] Last token: {:?}, is_cmd_context from token={}",
12721272- token.contents, matches
12731273- )));
12741274- matches
12751275- } else {
12761276- web_sys::console::log_1(&JsValue::from_str(
12771277- "[completion] No last token found, assuming start of input (is_cmd_context=true)",
12781278- ));
12791279- true // Start of input
12801280- };
12811281-12821282- // Look for the last non-whitespace token before cursor
12831283- let text_before = &input[..byte_pos];
12841284-12851285- // Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
12861286- let text_before_trimmed = text_before.trim_end();
12871287- let is_inside_block = text_before_trimmed.ends_with('{');
12881288- // If we found a separator after a command, we're starting a new command
12891289- let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
12901290- web_sys::console::log_1(&JsValue::from_str(&format!(
12911291- "[completion] is_inside_block={}, has_separator_after_command={}, final is_cmd_context={}",
12921292- is_inside_block, has_separator_after_command, is_cmd_context
12931293- )));
12941294-12951295- // Find the last word before cursor
12961296- let last_word_start = text_before
12971297- .rfind(|c: char| c.is_whitespace() || is_separator_char(c))
12981298- .map(|i| i + 1)
12991299- .unwrap_or(0);
13001300-13011301- let last_word = text_before[last_word_start..].trim_start();
13021302- web_sys::console::log_1(&JsValue::from_str(&format!(
13031303- "[completion] last_word_start={}, last_word={:?}",
13041304- last_word_start, last_word
13051305- )));
13061306-13071307- if is_cmd_context {
13081308- context = Some(CompletionContext::Command {
13091309- prefix: last_word.to_string(),
13101310- span: Span::new(last_word_start, byte_pos),
13111311- });
13121312- web_sys::console::log_1(&JsValue::from_str(&format!(
13131313- "[completion] Set Command context with prefix: {:?}",
13141314- last_word
13151315- )));
13161316- } else {
13171317- // Check if this is a variable or cell path (starts with $)
13181318- let trimmed_word = last_word.trim();
13191319- if trimmed_word.starts_with('$') {
13201320- // Check if this is a cell path (contains a dot after $)
13211321- if let Some(dot_pos) = trimmed_word[1..].find('.') {
13221322- // Cell path completion
13231323- let var_name = &trimmed_word[1..dot_pos + 1];
13241324- let after_var = &trimmed_word[dot_pos + 2..];
13251325- let parts: Vec<&str> = after_var.split('.').collect();
13261326- let (path_so_far, cell_prefix) = if parts.is_empty() {
13271327- (vec![], String::new())
13281328- } else if after_var.ends_with('.') {
13291329- (
13301330- parts
13311331- .iter()
13321332- .filter(|s| !s.is_empty())
13331333- .map(|s| s.to_string())
13341334- .collect(),
13351335- String::new(),
13361336- )
13371337- } else {
13381338- let path: Vec<String> = parts[..parts.len().saturating_sub(1)]
13391339- .iter()
13401340- .map(|s| s.to_string())
13411341- .collect();
13421342- let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default();
13431343- (path, prefix)
13441344- };
13451345-13461346- let var_id = match var_name {
13471347- "env" => Some(ENV_VARIABLE_ID),
13481348- "nu" => Some(NU_VARIABLE_ID),
13491349- "in" => Some(IN_VARIABLE_ID),
13501350- _ => working_set.find_variable(var_name.as_bytes()),
13511351- };
13521352-13531353- if let Some(var_id) = var_id {
13541354- let prefix_byte_len = cell_prefix.len();
13551355- let cell_span_start = byte_pos.saturating_sub(prefix_byte_len);
13561356- let cell_prefix_clone = cell_prefix.clone();
13571357- context = Some(CompletionContext::CellPath {
13581358- prefix: cell_prefix,
13591359- span: Span::new(cell_span_start, byte_pos),
13601360- var_id,
13611361- path_so_far,
13621362- });
13631363- web_sys::console::log_1(&JsValue::from_str(&format!(
13641364- "[completion] Set CellPath context with prefix: {:?}",
13651365- cell_prefix_clone
13661366- )));
13671367- } else {
13681368- let var_prefix = trimmed_word[1..].to_string();
13691369- let var_prefix_clone = var_prefix.clone();
13701370- context = Some(CompletionContext::Variable {
13711371- prefix: var_prefix,
13721372- span: Span::new(last_word_start, byte_pos),
13731373- });
13741374- web_sys::console::log_1(&JsValue::from_str(&format!(
13751375- "[completion] Set Variable context with prefix: {:?}",
13761376- var_prefix_clone
13771377- )));
13781378- }
13791379- } else {
13801380- // Simple variable completion
13811381- let var_prefix = trimmed_word[1..].to_string();
13821382- let var_prefix_clone = var_prefix.clone();
13831383- context = Some(CompletionContext::Variable {
13841384- prefix: var_prefix,
13851385- span: Span::new(last_word_start, byte_pos),
13861386- });
13871387- web_sys::console::log_1(&JsValue::from_str(&format!(
13881388- "[completion] Set Variable context with prefix: {:?}",
13891389- var_prefix_clone
13901390- )));
13911391- }
13921392- } else if trimmed_word.starts_with('-') {
13931393- // Try to find command by looking backwards through shapes
13941394- let mut found_cmd = None;
13951395- for (span, shape) in shapes.iter().rev() {
13961396- let local_span = to_local_span(*span);
13971397- if local_span.end <= byte_pos && is_command_shape(shape, local_span) {
13981398- let cmd_text = safe_slice(local_span);
13991399- let cmd_name = cmd_text
14001400- .split_whitespace()
14011401- .next()
14021402- .unwrap_or(&cmd_text)
14031403- .trim();
14041404- found_cmd = Some(cmd_name.to_string());
14051405- break;
14061406- }
14071407- }
14081408- if let Some(cmd_name) = found_cmd {
14091409- let cmd_name_clone = cmd_name.clone();
14101410- context = Some(CompletionContext::Flag {
14111411- prefix: trimmed_word.to_string(),
14121412- span: Span::new(last_word_start, byte_pos),
14131413- command_name: cmd_name,
14141414- });
14151415- web_sys::console::log_1(&JsValue::from_str(&format!(
14161416- "[completion] Set Flag context with prefix: {:?}, command: {:?}",
14171417- trimmed_word, cmd_name_clone
14181418- )));
14191419- } else {
14201420- context = Some(CompletionContext::Argument {
14211421- prefix: last_word.to_string(),
14221422- span: Span::new(last_word_start, byte_pos),
14231423- });
14241424- web_sys::console::log_1(&JsValue::from_str(&format!(
14251425- "[completion] Set Argument context with prefix: {:?}",
14261426- last_word
14271427- )));
14281428- }
14291429- } else {
14301430- // Try to find command and argument index
14311431- let mut found_cmd = None;
14321432- let mut arg_count = 0;
14331433- for (span, shape) in shapes.iter().rev() {
14341434- let local_span = to_local_span(*span);
14351435- if local_span.end <= byte_pos {
14361436- if is_command_shape(shape, local_span) {
14371437- let cmd_text = safe_slice(local_span);
14381438- let cmd_name = cmd_text
14391439- .split_whitespace()
14401440- .next()
14411441- .unwrap_or(&cmd_text)
14421442- .trim();
14431443- found_cmd = Some(cmd_name.to_string());
14441444- break;
14451445- } else {
14461446- let arg_text = safe_slice(local_span);
14471447- let trimmed_arg = arg_text.trim();
14481448- if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
14491449- arg_count += 1;
14501450- }
14511451- }
14521452- }
14531453- }
14541454- if let Some(cmd_name) = found_cmd {
14551455- let cmd_name_clone = cmd_name.clone();
14561456- context = Some(CompletionContext::CommandArgument {
14571457- prefix: trimmed_word.to_string(),
14581458- span: Span::new(last_word_start, byte_pos),
14591459- command_name: cmd_name,
14601460- arg_index: arg_count,
14611461- });
14621462- web_sys::console::log_1(&JsValue::from_str(&format!(
14631463- "[completion] Set CommandArgument context with prefix: {:?}, command: {:?}, arg_index: {}",
14641464- trimmed_word, cmd_name_clone, arg_count
14651465- )));
14661466- } else {
14671467- context = Some(CompletionContext::Argument {
14681468- prefix: last_word.to_string(),
14691469- span: Span::new(last_word_start, byte_pos),
14701470- });
14711471- web_sys::console::log_1(&JsValue::from_str(&format!(
14721472- "[completion] Set Argument context with prefix: {:?}",
14731473- last_word
14741474- )));
14751475- }
14761476- }
14771477- }
14781478- }
14791479- }
14801480-14811481- web_sys::console::log_1(&JsValue::from_str(&format!("context: {:?}", context)));
14821482-14831483- let mut suggestions: Vec<Suggestion> = Vec::new();
14841484-14851485- // Convert byte-spans back to char-spans for JS
14861486- let to_char_span = |span: Span| -> Span {
14871487- let char_start = input[..span.start].chars().count();
14881488- let char_end = input[..span.end].chars().count();
14891489- Span::new(char_start, char_end)
14901490- };
14911491-14921492- match context {
14931493- Some(CompletionContext::Command { prefix, span }) => {
14941494- web_sys::console::log_1(&JsValue::from_str(&format!(
14951495- "[completion] Generating Command suggestions with prefix: {:?}",
14961496- prefix
14971497- )));
14981498- // Command completion
14991499- let cmds = working_set
15001500- .find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true);
15011501-15021502- let span = to_char_span(span);
15031503- let mut cmd_count = 0;
15041504-15051505- for (_, name, desc, _) in cmds {
15061506- let name_str = String::from_utf8_lossy(&name).to_string();
15071507- suggestions.push(Suggestion {
15081508- rendered: {
15091509- let name_colored = ansi_term::Color::Green.bold().paint(&name_str);
15101510- let desc_str = desc.as_deref().unwrap_or("<no description>");
15111511- format!("{name_colored} {desc_str}")
15121512- },
15131513- name: name_str,
15141514- description: desc,
15151515- is_command: true,
15161516- span_start: span.start,
15171517- span_end: span.end,
15181518- });
15191519- cmd_count += 1;
15201520- }
15211521- web_sys::console::log_1(&JsValue::from_str(&format!(
15221522- "[completion] Found {} command suggestions",
15231523- cmd_count
15241524- )));
15251525- }
15261526- Some(CompletionContext::Argument { prefix, span }) => {
15271527- web_sys::console::log_1(&JsValue::from_str(&format!(
15281528- "[completion] Generating Argument suggestions with prefix: {:?}",
15291529- prefix
15301530- )));
15311531- // File completion
15321532- let (dir, file_prefix) = prefix
15331533- .rfind('/')
15341534- .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
15351535- .unwrap_or(("", prefix.as_str()));
15361536-15371537- let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
15381538- .then(|| &dir[..dir.len() - 1])
15391539- .unwrap_or(dir);
15401540-15411541- let target_dir = if !dir.is_empty() {
15421542- match root.join(dir_to_join) {
15431543- Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
15441544- _ => None,
15451545- }
15461546- } else {
15471547- Some(root.join("").unwrap())
15481548- };
15491549-15501550- let mut file_count = 0;
15511551- if let Some(d) = target_dir {
15521552- if let Ok(iterator) = d.read_dir() {
15531553- let span = to_char_span(span);
15541554-15551555- for entry in iterator {
15561556- let name = entry.filename();
15571557- if name.starts_with(file_prefix) {
15581558- let full_completion = format!("{}{}", dir, name);
15591559- suggestions.push(Suggestion {
15601560- name: full_completion.clone(),
15611561- description: None,
15621562- is_command: false,
15631563- rendered: full_completion,
15641564- span_start: span.start,
15651565- span_end: span.end,
15661566- });
15671567- file_count += 1;
15681568- }
15691569- }
15701570- }
15711571- }
15721572- web_sys::console::log_1(&JsValue::from_str(&format!(
15731573- "[completion] Found {} file suggestions",
15741574- file_count
15751575- )));
15761576- }
15771577- Some(CompletionContext::Flag {
15781578- prefix,
15791579- span,
15801580- command_name,
15811581- }) => {
15821582- web_sys::console::log_1(&JsValue::from_str(&format!(
15831583- "[completion] Generating Flag suggestions for command: {:?}, prefix: {:?}",
15841584- command_name, prefix
15851585- )));
15861586-15871587- if let Some(signature) = get_command_signature(&command_name) {
15881588- let span = to_char_span(span);
15891589- let mut flag_count = 0;
15901590-15911591- // Get switches from signature
15921592- // Signature has a named field that contains named arguments (including switches)
15931593- for flag in &signature.named {
15941594- // Check if this is a switch (has no argument)
15951595- // Switches have arg: None, named arguments have arg: Some(SyntaxShape)
15961596- let is_switch = flag.arg.is_none();
15971597-15981598- if is_switch {
15991599- let long_name = format!("--{}", flag.long);
16001600- let short_name = flag.short.map(|c| format!("-{}", c));
16011601-16021602- // Determine which flags to show based on prefix:
16031603- // - If prefix is empty or exactly "-", show all flags (both short and long)
16041604- // - If prefix starts with "--", only show long flags that match the prefix
16051605- // - If prefix starts with "-" (but not "--"), only show short flags that match the prefix
16061606- let show_all = prefix.is_empty() || prefix == "-";
16071607-16081608- // Helper to create a flag suggestion
16091609- let create_flag_suggestion = |flag_name: String| -> Suggestion {
16101610- Suggestion {
16111611- name: flag_name.clone(),
16121612- description: Some(flag.desc.clone()),
16131613- is_command: false,
16141614- rendered: {
16151615- let flag_colored =
16161616- ansi_term::Color::Cyan.bold().paint(&flag_name);
16171617- format!("{flag_colored} {}", flag.desc)
16181618- },
16191619- span_start: span.start,
16201620- span_end: span.end,
16211621- }
16221622- };
16231623-16241624- // Add long flag if it matches
16251625- let should_show_long = if show_all {
16261626- true // Show all flags when prefix is "-" or empty
16271627- } else if prefix.starts_with("--") {
16281628- long_name.starts_with(&prefix) // Only show long flags matching prefix
16291629- } else {
16301630- false // Don't show long flags if prefix is short flag format
16311631- };
16321632-16331633- if should_show_long {
16341634- suggestions.push(create_flag_suggestion(long_name));
16351635- flag_count += 1;
16361636- }
16371637-16381638- // Add short flag if it matches
16391639- if let Some(short) = &short_name {
16401640- let should_show_short = if show_all {
16411641- true // Show all flags when prefix is "-" or empty
16421642- } else if prefix.starts_with("-") && !prefix.starts_with("--") {
16431643- short.starts_with(&prefix) // Only show short flags matching prefix
16441644- } else {
16451645- false // Don't show short flags if prefix is long flag format
16461646- };
16471647-16481648- if should_show_short {
16491649- suggestions.push(create_flag_suggestion(short.clone()));
16501650- flag_count += 1;
16511651- }
16521652- }
16531653- }
16541654- }
16551655-16561656- web_sys::console::log_1(&JsValue::from_str(&format!(
16571657- "[completion] Found {} flag suggestions",
16581658- flag_count
16591659- )));
16601660- } else {
16611661- web_sys::console::log_1(&JsValue::from_str(&format!(
16621662- "[completion] Could not find signature for command: {:?}",
16631663- command_name
16641664- )));
16651665- }
16661666- }
16671667- Some(CompletionContext::CommandArgument {
16681668- prefix,
16691669- span,
16701670- command_name,
16711671- arg_index,
16721672- }) => {
16731673- web_sys::console::log_1(&JsValue::from_str(&format!(
16741674- "[completion] Generating CommandArgument suggestions for command: {:?}, arg_index: {}, prefix: {:?}",
16751675- command_name, arg_index, prefix
16761676- )));
16771677-16781678- if let Some(signature) = get_command_signature(&command_name) {
16791679- // Get positional arguments from signature
16801680- // Combine required and optional positional arguments
16811681- let mut all_positional = Vec::new();
16821682- all_positional.extend_from_slice(&signature.required_positional);
16831683- all_positional.extend_from_slice(&signature.optional_positional);
16841684-16851685- // Find the argument at the given index
16861686- if let Some(arg) = all_positional.get(arg_index) {
16871687- // Check the SyntaxShape to determine completion type
16881688- match &arg.shape {
16891689- nu_protocol::SyntaxShape::String | nu_protocol::SyntaxShape::Filepath => {
16901690- // File/directory completion
16911691- let (dir, file_prefix) = prefix
16921692- .rfind('/')
16931693- .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
16941694- .unwrap_or(("", prefix.as_str()));
16951695-16961696- let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
16971697- .then(|| &dir[..dir.len() - 1])
16981698- .unwrap_or(dir);
16991699-17001700- let target_dir = if !dir.is_empty() {
17011701- match root.join(dir_to_join) {
17021702- Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
17031703- _ => None,
17041704- }
17051705- } else {
17061706- Some(root.join("").unwrap())
17071707- };
17081708-17091709- let span = to_char_span(span);
17101710- let mut file_count = 0;
17111711- if let Some(d) = target_dir {
17121712- if let Ok(iterator) = d.read_dir() {
17131713- for entry in iterator {
17141714- let name = entry.filename();
17151715- if name.starts_with(file_prefix) {
17161716- let full_completion = format!("{}{}", dir, name);
17171717- suggestions.push(Suggestion {
17181718- name: full_completion.clone(),
17191719- description: Some(arg.desc.clone()),
17201720- is_command: false,
17211721- rendered: full_completion,
17221722- span_start: span.start,
17231723- span_end: span.end,
17241724- });
17251725- file_count += 1;
17261726- }
17271727- }
17281728- }
17291729- }
17301730- web_sys::console::log_1(&JsValue::from_str(&format!(
17311731- "[completion] Found {} file suggestions for argument {}",
17321732- file_count, arg_index
17331733- )));
17341734- }
17351735- _ => {
17361736- // For other types, fall back to file completion
17371737- let (dir, file_prefix) = prefix
17381738- .rfind('/')
17391739- .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
17401740- .unwrap_or(("", prefix.as_str()));
17411741-17421742- let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
17431743- .then(|| &dir[..dir.len() - 1])
17441744- .unwrap_or(dir);
17451745-17461746- let target_dir = if !dir.is_empty() {
17471747- match root.join(dir_to_join) {
17481748- Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
17491749- _ => None,
17501750- }
17511751- } else {
17521752- Some(root.join("").unwrap())
17531753- };
17541754-17551755- let span = to_char_span(span);
17561756- if let Some(d) = target_dir {
17571757- if let Ok(iterator) = d.read_dir() {
17581758- for entry in iterator {
17591759- let name = entry.filename();
17601760- if name.starts_with(file_prefix) {
17611761- let full_completion = format!("{}{}", dir, name);
17621762- suggestions.push(Suggestion {
17631763- name: full_completion.clone(),
17641764- description: Some(arg.desc.clone()),
17651765- is_command: false,
17661766- rendered: full_completion,
17671767- span_start: span.start,
17681768- span_end: span.end,
17691769- });
17701770- }
17711771- }
17721772- }
17731773- }
17741774- }
17751775- }
17761776- } else {
17771777- // Argument index out of range, fall back to file completion
17781778- web_sys::console::log_1(&JsValue::from_str(&format!(
17791779- "[completion] Argument index {} out of range, using file completion",
17801780- arg_index
17811781- )));
17821782- // Use the same file completion logic as Argument context
17831783- let (dir, file_prefix) = prefix
17841784- .rfind('/')
17851785- .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
17861786- .unwrap_or(("", prefix.as_str()));
17871787-17881788- let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
17891789- .then(|| &dir[..dir.len() - 1])
17901790- .unwrap_or(dir);
17911791-17921792- let target_dir = if !dir.is_empty() {
17931793- match root.join(dir_to_join) {
17941794- Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
17951795- _ => None,
17961796- }
17971797- } else {
17981798- Some(root.join("").unwrap())
17991799- };
18001800-18011801- let span = to_char_span(span);
18021802- if let Some(d) = target_dir {
18031803- if let Ok(iterator) = d.read_dir() {
18041804- for entry in iterator {
18051805- let name = entry.filename();
18061806- if name.starts_with(file_prefix) {
18071807- let full_completion = format!("{}{}", dir, name);
18081808- suggestions.push(Suggestion {
18091809- name: full_completion.clone(),
18101810- description: None,
18111811- is_command: false,
18121812- rendered: full_completion,
18131813- span_start: span.start,
18141814- span_end: span.end,
18151815- });
18161816- }
18171817- }
18181818- }
18191819- }
18201820- }
18211821- } else {
18221822- // No signature found, fall back to file completion
18231823- web_sys::console::log_1(&JsValue::from_str(&format!(
18241824- "[completion] Could not find signature for command: {:?}, using file completion",
18251825- command_name
18261826- )));
18271827- let (dir, file_prefix) = prefix
18281828- .rfind('/')
18291829- .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
18301830- .unwrap_or(("", prefix.as_str()));
18311831-18321832- let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
18331833- .then(|| &dir[..dir.len() - 1])
18341834- .unwrap_or(dir);
18351835-18361836- let target_dir = if !dir.is_empty() {
18371837- match root.join(dir_to_join) {
18381838- Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
18391839- _ => None,
18401840- }
18411841- } else {
18421842- Some(root.join("").unwrap())
18431843- };
18441844-18451845- let span = to_char_span(span);
18461846- if let Some(d) = target_dir {
18471847- if let Ok(iterator) = d.read_dir() {
18481848- for entry in iterator {
18491849- let name = entry.filename();
18501850- if name.starts_with(file_prefix) {
18511851- let full_completion = format!("{}{}", dir, name);
18521852- suggestions.push(Suggestion {
18531853- name: full_completion.clone(),
18541854- description: None,
18551855- is_command: false,
18561856- rendered: full_completion,
18571857- span_start: span.start,
18581858- span_end: span.end,
18591859- });
18601860- }
18611861- }
18621862- }
18631863- }
18641864- }
18651865- }
18661866- Some(CompletionContext::Variable { prefix, span }) => {
18671867- web_sys::console::log_1(&JsValue::from_str(&format!(
18681868- "[completion] Generating Variable suggestions with prefix: {:?}",
18691869- prefix
18701870- )));
18711871-18721872- // Collect all available variables
18731873- let variables = collect_variables(&working_set, &input, byte_pos);
18741874- let span = to_char_span(span);
18751875- let mut var_count = 0;
18761876-18771877- for (var_name, var_id) in variables {
18781878- // Filter by prefix (variable name includes $, so we need to check after $)
18791879- if var_name.len() > 1 && var_name[1..].starts_with(&prefix) {
18801880- // Get variable type
18811881- let var_type = working_set.get_variable(var_id).ty.to_string();
18821882-18831883- suggestions.push(Suggestion {
18841884- name: var_name.clone(),
18851885- description: Some(var_type.clone()),
18861886- is_command: false,
18871887- rendered: {
18881888- let var_colored = ansi_term::Color::Blue.bold().paint(&var_name);
18891889- format!("{var_colored} {var_type}")
18901890- },
18911891- span_start: span.start,
18921892- span_end: span.end,
18931893- });
18941894- var_count += 1;
18951895- }
18961896- }
18971897-18981898- web_sys::console::log_1(&JsValue::from_str(&format!(
18991899- "[completion] Found {} variable suggestions",
19001900- var_count
19011901- )));
19021902- }
19031903- Some(CompletionContext::CellPath {
19041904- prefix,
19051905- span,
19061906- var_id,
19071907- path_so_far,
19081908- }) => {
19091909- web_sys::console::log_1(&JsValue::from_str(&format!(
19101910- "[completion] Generating CellPath suggestions with prefix: {:?}, path: {:?}",
19111911- prefix, path_so_far
19121912- )));
19131913-19141914- // Evaluate the variable to get its value
19151915- if let Some(var_value) = eval_variable_for_completion(var_id, &working_set) {
19161916- // Follow the path to get the value at the current level
19171917- let current_value = if path_so_far.is_empty() {
19181918- var_value
19191919- } else {
19201920- follow_cell_path(&var_value, &path_so_far).unwrap_or(var_value)
19211921- };
19221922-19231923- // Get columns/fields from the current value
19241924- let columns = get_columns_from_value(¤t_value);
19251925- let span = to_char_span(span);
19261926- let mut field_count = 0;
19271927-19281928- for (col_name, col_type) in columns {
19291929- // Filter by prefix
19301930- if col_name.starts_with(&prefix) {
19311931- let type_str = col_type.as_deref().unwrap_or("any");
19321932- suggestions.push(Suggestion {
19331933- name: col_name.clone(),
19341934- description: Some(type_str.to_string()),
19351935- is_command: false,
19361936- rendered: {
19371937- let col_colored = ansi_term::Color::Yellow.paint(&col_name);
19381938- format!("{col_colored} {type_str}")
19391939- },
19401940- span_start: span.start,
19411941- span_end: span.end,
19421942- });
19431943- field_count += 1;
19441944- }
19451945- }
19461946-19471947- web_sys::console::log_1(&JsValue::from_str(&format!(
19481948- "[completion] Found {} cell path suggestions",
19491949- field_count
19501950- )));
19511951- } else {
19521952- // Variable couldn't be evaluated - this is expected for runtime variables
19531953- // We can't provide cell path completions without knowing the structure
19541954- web_sys::console::log_1(&JsValue::from_str(&format!(
19551955- "[completion] Could not evaluate variable {:?} for cell path completion (runtime variable)",
19561956- var_id
19571957- )));
19581958-19591959- // Try to get type information to provide better feedback
19601960- if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
19611961- working_set.get_variable(var_id)
19621962- })) {
19631963- web_sys::console::log_1(&JsValue::from_str(&format!(
19641964- "[completion] Variable type: {:?}",
19651965- var_info.ty
19661966- )));
19671967- }
19681968- }
19691969- }
19701970- _ => {
19711971- web_sys::console::log_1(&JsValue::from_str(
19721972- "[completion] Context is None, no suggestions generated",
19731973- ));
19741974- }
19751975- }
19761976-19771977- drop(working_set);
19781978- drop(engine_guard);
19791979-19801980- suggestions.sort();
19811981- let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string());
19821982- web_sys::console::log_1(&JsValue::from_str(&suggestions));
19831983- suggestions
19841984-}
+949
src/completion/context.rs
···11+use crate::completion::helpers::*;
22+use crate::completion::types::CompletionContext;
33+use crate::console_log;
44+use nu_parser::FlatShape;
55+use nu_protocol::engine::{EngineState, StateWorkingSet};
66+use nu_protocol::{Signature, Span};
77+88+pub fn find_command_and_arg_index(
99+ input: &str,
1010+ shapes: &[(Span, FlatShape)],
1111+ current_idx: usize,
1212+ current_local_span: Span,
1313+ global_offset: usize,
1414+) -> Option<(String, usize)> {
1515+ let mut command_name: Option<String> = None;
1616+ let mut arg_count = 0;
1717+1818+ // Look backwards through shapes to find the command
1919+ for i in (0..current_idx).rev() {
2020+ if let Some((prev_span, prev_shape)) = shapes.get(i) {
2121+ let prev_local_span = to_local_span(*prev_span, global_offset);
2222+2323+ // Check if there's a separator between this shape and the next one
2424+ let next_shape_start = if i + 1 < shapes.len() {
2525+ to_local_span(shapes[i + 1].0, global_offset).start
2626+ } else {
2727+ current_local_span.start
2828+ };
2929+3030+ if has_separator_between(input, prev_local_span.end, next_shape_start) {
3131+ break; // Stop at separator
3232+ }
3333+3434+ if is_command_shape(input, prev_shape, prev_local_span) {
3535+ // Found the command
3636+ let cmd_text = safe_slice(input, prev_local_span);
3737+ let cmd_name = extract_command_name(cmd_text);
3838+ command_name = Some(cmd_name.to_string());
3939+ break;
4040+ } else {
4141+ // This is an argument - count it if it's not a flag
4242+ let arg_text = safe_slice(input, prev_local_span);
4343+ let trimmed_arg = arg_text.trim();
4444+ // Don't count flags (starting with -) or empty arguments
4545+ if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
4646+ arg_count += 1;
4747+ }
4848+ }
4949+ }
5050+ }
5151+5252+ command_name.map(|name| (name, arg_count))
5353+}
5454+5555+pub fn build_command_prefix(
5656+ input: &str,
5757+ shapes: &[(Span, FlatShape)],
5858+ current_idx: usize,
5959+ current_local_span: Span,
6060+ current_prefix: &str,
6161+ global_offset: usize,
6262+) -> (String, Span) {
6363+ let mut span_start = current_local_span.start;
6464+6565+ // Look backwards through shapes to find previous command words
6666+ for i in (0..current_idx).rev() {
6767+ if let Some((prev_span, prev_shape)) = shapes.get(i) {
6868+ let prev_local_span = to_local_span(*prev_span, global_offset);
6969+7070+ if is_command_shape(input, prev_shape, prev_local_span) {
7171+ // Check if there's a separator between this shape and the next one
7272+ let next_shape_start = if i + 1 < shapes.len() {
7373+ to_local_span(shapes[i + 1].0, global_offset).start
7474+ } else {
7575+ current_local_span.start
7676+ };
7777+7878+ // Check if there's a separator (pipe, semicolon, etc.) between shapes
7979+ // Whitespace is fine, but separators indicate a new command
8080+ if has_separator_between(input, prev_local_span.end, next_shape_start) {
8181+ break; // Stop at separator
8282+ }
8383+8484+ // Update span start to include this command word
8585+ span_start = prev_local_span.start;
8686+ } else {
8787+ // Not a command shape, stop looking backwards
8888+ break;
8989+ }
9090+ }
9191+ }
9292+9393+ // Extract the full prefix from the input, preserving exact spacing
9494+ let span_end = current_local_span.end;
9595+ let full_prefix = if span_start < input.len() {
9696+ safe_slice(input, Span::new(span_start, span_end)).to_string()
9797+ } else {
9898+ current_prefix.to_string()
9999+ };
100100+101101+ (full_prefix, Span::new(span_start, span_end))
102102+}
103103+104104+pub fn get_command_signature(engine_guard: &EngineState, cmd_name: &str) -> Option<Signature> {
105105+ engine_guard
106106+ .find_decl(cmd_name.as_bytes(), &[])
107107+ .map(|id| engine_guard.get_decl(id).signature())
108108+}
109109+110110+pub fn determine_flag_or_argument_context(
111111+ input: &str,
112112+ shapes: &[(Span, FlatShape)],
113113+ prefix: &str,
114114+ idx: usize,
115115+ local_span: Span,
116116+ span: Span,
117117+ global_offset: usize,
118118+) -> CompletionContext {
119119+ let trimmed_prefix = prefix.trim();
120120+ if trimmed_prefix.starts_with('-') {
121121+ // This looks like a flag - find the command
122122+ if let Some((cmd_name, _)) =
123123+ find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
124124+ {
125125+ CompletionContext::Flag {
126126+ prefix: trimmed_prefix.to_string(),
127127+ span,
128128+ command_name: cmd_name,
129129+ }
130130+ } else {
131131+ CompletionContext::Argument {
132132+ prefix: prefix.to_string(),
133133+ span,
134134+ }
135135+ }
136136+ } else {
137137+ // This is a positional argument - find the command and argument index
138138+ if let Some((cmd_name, arg_index)) =
139139+ find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
140140+ {
141141+ CompletionContext::CommandArgument {
142142+ prefix: trimmed_prefix.to_string(),
143143+ span,
144144+ command_name: cmd_name,
145145+ arg_index,
146146+ }
147147+ } else {
148148+ CompletionContext::Argument {
149149+ prefix: prefix.to_string(),
150150+ span,
151151+ }
152152+ }
153153+ }
154154+}
155155+156156+pub fn handle_block_or_closure(
157157+ input: &str,
158158+ shapes: &[(Span, FlatShape)],
159159+ working_set: &StateWorkingSet,
160160+ prefix: &str,
161161+ span: Span,
162162+ shape_name: &str,
163163+ current_idx: usize,
164164+ local_span: Span,
165165+ global_offset: usize,
166166+) -> Option<CompletionContext> {
167167+ console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}");
168168+169169+ // Check if the content ends with a pipe or semicolon
170170+ let prefix_ends_with_separator = ends_with_separator(prefix);
171171+ let last_sep_pos_in_prefix = if prefix_ends_with_separator {
172172+ find_last_separator_pos(prefix)
173173+ } else {
174174+ None
175175+ };
176176+ console_log!(
177177+ "[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}"
178178+ );
179179+180180+ if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
181181+ console_log!(
182182+ "[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}"
183183+ );
184184+185185+ if is_empty {
186186+ // Empty block/closure or just whitespace - command context
187187+ console_log!("[completion] {shape_name} is empty, setting Command context");
188188+ Some(CompletionContext::Command {
189189+ prefix: String::new(),
190190+ span: adjusted_span,
191191+ })
192192+ } else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
193193+ // After a separator - command context
194194+ let after_sep = prefix[last_sep_pos..].trim_start();
195195+ console_log!(
196196+ "[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context"
197197+ );
198198+ Some(CompletionContext::Command {
199199+ prefix: after_sep.to_string(),
200200+ span: Span::new(span.start + last_sep_pos, span.end),
201201+ })
202202+ } else {
203203+ console_log!(
204204+ "[completion] {shape_name} has no separator, checking for variable/flag/argument context"
205205+ );
206206+ // Check if this is a variable or cell path first
207207+ let trimmed = trimmed_prefix.trim();
208208+209209+ if trimmed.starts_with('$') {
210210+ // Variable or cell path completion
211211+ if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) {
212212+ let var_id = lookup_variable_id(var_name, working_set);
213213+214214+ if let Some(var_id) = var_id {
215215+ let prefix_byte_len = cell_prefix.len();
216216+ let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len);
217217+ console_log!(
218218+ "[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}"
219219+ );
220220+ Some(CompletionContext::CellPath {
221221+ prefix: cell_prefix.to_string(),
222222+ span: Span::new(cell_span_start, adjusted_span.end),
223223+ var_id,
224224+ path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
225225+ })
226226+ } else {
227227+ // Unknown variable, fall back to variable completion
228228+ let var_prefix = trimmed[1..].to_string();
229229+ console_log!(
230230+ "[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}"
231231+ );
232232+ Some(CompletionContext::Variable {
233233+ prefix: var_prefix,
234234+ span: adjusted_span,
235235+ })
236236+ }
237237+ } else {
238238+ // Simple variable completion (no dot)
239239+ let var_prefix = if trimmed.len() > 1 {
240240+ trimmed[1..].to_string()
241241+ } else {
242242+ String::new()
243243+ };
244244+ console_log!(
245245+ "[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}"
246246+ );
247247+ Some(CompletionContext::Variable {
248248+ prefix: var_prefix,
249249+ span: adjusted_span,
250250+ })
251251+ }
252252+ } else if trimmed.starts_with('-') {
253253+ // Flag completion
254254+ if let Some((cmd_name, _)) = find_command_and_arg_index(
255255+ input,
256256+ shapes,
257257+ current_idx,
258258+ local_span,
259259+ global_offset,
260260+ ) {
261261+ console_log!(
262262+ "[completion] {shape_name}: Found command {cmd_name:?} for flag completion"
263263+ );
264264+ Some(CompletionContext::Flag {
265265+ prefix: trimmed.to_string(),
266266+ span: adjusted_span,
267267+ command_name: cmd_name,
268268+ })
269269+ } else {
270270+ Some(CompletionContext::Argument {
271271+ prefix: trimmed_prefix.to_string(),
272272+ span: adjusted_span,
273273+ })
274274+ }
275275+ } else {
276276+ // Try to find the command and argument index
277277+ if let Some((cmd_name, arg_index)) = find_command_and_arg_index(
278278+ input,
279279+ shapes,
280280+ current_idx,
281281+ local_span,
282282+ global_offset,
283283+ ) {
284284+ console_log!(
285285+ "[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion"
286286+ );
287287+ Some(CompletionContext::CommandArgument {
288288+ prefix: trimmed.to_string(),
289289+ span: adjusted_span,
290290+ command_name: cmd_name,
291291+ arg_index,
292292+ })
293293+ } else {
294294+ // No command found, treat as regular argument
295295+ console_log!(
296296+ "[completion] {shape_name}: No command found, using Argument context"
297297+ );
298298+ Some(CompletionContext::Argument {
299299+ prefix: trimmed_prefix.to_string(),
300300+ span: adjusted_span,
301301+ })
302302+ }
303303+ }
304304+ }
305305+ } else {
306306+ None
307307+ }
308308+}
309309+310310+pub fn handle_variable_string_shape(
311311+ input: &str,
312312+ shapes: &[(Span, FlatShape)],
313313+ _working_set: &StateWorkingSet,
314314+ idx: usize,
315315+ prefix: &str,
316316+ span: Span,
317317+ local_span: Span,
318318+ global_offset: usize,
319319+) -> Option<CompletionContext> {
320320+ if idx == 0 {
321321+ return None;
322322+ }
323323+324324+ let prev_shape = &shapes[idx - 1];
325325+ let prev_local_span = to_local_span(prev_shape.0, global_offset);
326326+327327+ if let FlatShape::Variable(var_id) = prev_shape.1 {
328328+ // Check if the variable shape ends right where this shape starts (or very close)
329329+ // Allow for a small gap (like a dot) between shapes
330330+ let gap = local_span.start.saturating_sub(prev_local_span.end);
331331+ if gap <= 1 {
332332+ // This is a cell path - the String shape contains the field name(s)
333333+ // The prefix might be like "na" or "field.subfield"
334334+ let trimmed_prefix = prefix.trim();
335335+ let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix);
336336+337337+ let prefix_byte_len = cell_prefix.len();
338338+ let cell_span_start = span.end.saturating_sub(prefix_byte_len);
339339+ console_log!(
340340+ "[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}"
341341+ );
342342+ Some(CompletionContext::CellPath {
343343+ prefix: cell_prefix.to_string(),
344344+ span: Span::new(cell_span_start, span.end),
345345+ var_id,
346346+ path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
347347+ })
348348+ } else {
349349+ // Gap between shapes, use helper to determine context
350350+ Some(determine_flag_or_argument_context(
351351+ input,
352352+ shapes,
353353+ &prefix.trim(),
354354+ idx,
355355+ local_span,
356356+ span,
357357+ global_offset,
358358+ ))
359359+ }
360360+ } else {
361361+ // Previous shape is not a Variable, use helper to determine context
362362+ Some(determine_flag_or_argument_context(
363363+ input,
364364+ shapes,
365365+ &prefix.trim(),
366366+ idx,
367367+ local_span,
368368+ span,
369369+ global_offset,
370370+ ))
371371+ }
372372+}
373373+374374+pub fn handle_dot_shape(
375375+ _input: &str,
376376+ shapes: &[(Span, FlatShape)],
377377+ idx: usize,
378378+ prefix: &str,
379379+ span: Span,
380380+ local_span: Span,
381381+ global_offset: usize,
382382+) -> Option<CompletionContext> {
383383+ if idx == 0 {
384384+ return Some(CompletionContext::Argument {
385385+ prefix: prefix.to_string(),
386386+ span,
387387+ });
388388+ }
389389+390390+ let prev_shape = &shapes[idx - 1];
391391+ let prev_local_span = to_local_span(prev_shape.0, global_offset);
392392+393393+ if let FlatShape::Variable(var_id) = prev_shape.1 {
394394+ // Check if the variable shape ends right where this shape starts
395395+ if prev_local_span.end == local_span.start {
396396+ let trimmed_prefix = prefix.trim();
397397+ // Parse path members from the prefix (which is like ".field" or ".field.subfield")
398398+ let after_dot = &trimmed_prefix[1..]; // Remove leading dot
399399+ let (path_so_far, cell_prefix) = if after_dot.is_empty() {
400400+ (vec![], "")
401401+ } else {
402402+ parse_cell_path_from_fields(after_dot)
403403+ };
404404+405405+ let prefix_byte_len = cell_prefix.len();
406406+ let cell_span_start = span.end.saturating_sub(prefix_byte_len);
407407+ console_log!(
408408+ "[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}"
409409+ );
410410+ Some(CompletionContext::CellPath {
411411+ prefix: cell_prefix.to_string(),
412412+ span: Span::new(cell_span_start, span.end),
413413+ var_id,
414414+ path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
415415+ })
416416+ } else {
417417+ // Gap between shapes, fall through to default handling
418418+ Some(CompletionContext::Argument {
419419+ prefix: prefix.to_string(),
420420+ span,
421421+ })
422422+ }
423423+ } else {
424424+ // Previous shape is not a Variable, this is likely a file path starting with .
425425+ Some(CompletionContext::Argument {
426426+ prefix: prefix.to_string(),
427427+ span,
428428+ })
429429+ }
430430+}
431431+432432+pub fn determine_context_from_shape(
433433+ input: &str,
434434+ shapes: &[(Span, FlatShape)],
435435+ working_set: &StateWorkingSet,
436436+ byte_pos: usize,
437437+ global_offset: usize,
438438+) -> Option<CompletionContext> {
439439+ // First, check if cursor is within a shape
440440+ for (idx, (span, shape)) in shapes.iter().enumerate() {
441441+ let local_span = to_local_span(*span, global_offset);
442442+443443+ if local_span.start <= byte_pos && byte_pos <= local_span.end {
444444+ console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}");
445445+446446+ // Check if there's a pipe or semicolon between this shape's end and the cursor
447447+ // If so, we're starting a new command and should ignore this shape
448448+ let has_sep = has_separator_between(input, local_span.end, byte_pos);
449449+ if has_sep {
450450+ console_log!(
451451+ "[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape",
452452+ end = local_span.end
453453+ );
454454+ // There's a separator, so we're starting a new command - skip this shape
455455+ continue;
456456+ }
457457+458458+ let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
459459+ let prefix = safe_slice(input, span);
460460+ console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}");
461461+462462+ // Special case: if prefix is just '{' (possibly with whitespace),
463463+ // we're at the start of a block and should complete commands
464464+ let trimmed_prefix = prefix.trim();
465465+ if trimmed_prefix == "{" {
466466+ // We're right after '{' - command context
467467+ if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
468468+ return Some(CompletionContext::Command {
469469+ prefix: String::new(),
470470+ span: adjusted_span,
471471+ });
472472+ }
473473+ } else {
474474+ match shape {
475475+ // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes
476476+ _ if { idx > 0 && matches!(shape, FlatShape::String) } => {
477477+ if let Some(ctx) = handle_variable_string_shape(
478478+ input,
479479+ shapes,
480480+ working_set,
481481+ idx,
482482+ &prefix,
483483+ span,
484484+ local_span,
485485+ global_offset,
486486+ ) {
487487+ return Some(ctx);
488488+ }
489489+ }
490490+ // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes
491491+ _ if {
492492+ let trimmed_prefix = prefix.trim();
493493+ trimmed_prefix.starts_with('.') && idx > 0
494494+ } =>
495495+ {
496496+ if let Some(ctx) = handle_dot_shape(
497497+ input,
498498+ shapes,
499499+ idx,
500500+ &prefix,
501501+ span,
502502+ local_span,
503503+ global_offset,
504504+ ) {
505505+ return Some(ctx);
506506+ }
507507+ }
508508+ _ if {
509509+ // Check if this is a variable or cell path (starts with $) before treating as command
510510+ let trimmed_prefix = prefix.trim();
511511+ trimmed_prefix.starts_with('$')
512512+ } =>
513513+ {
514514+ let trimmed_prefix = prefix.trim();
515515+ // Check if this is a cell path (contains a dot after $)
516516+ if let Some((var_name, path_so_far, cell_prefix)) =
517517+ parse_cell_path(trimmed_prefix)
518518+ {
519519+ // Find the variable ID
520520+ let var_id = lookup_variable_id(var_name, working_set);
521521+522522+ if let Some(var_id) = var_id {
523523+ // Calculate span for the cell path member being completed
524524+ let prefix_byte_len = cell_prefix.len();
525525+ let cell_span_start = span.end.saturating_sub(prefix_byte_len);
526526+ return Some(CompletionContext::CellPath {
527527+ prefix: cell_prefix.to_string(),
528528+ span: Span::new(cell_span_start, span.end),
529529+ var_id,
530530+ path_so_far: path_so_far
531531+ .iter()
532532+ .map(|s| s.to_string())
533533+ .collect(),
534534+ });
535535+ } else {
536536+ // Unknown variable, fall back to variable completion
537537+ let var_prefix = trimmed_prefix[1..].to_string();
538538+ return Some(CompletionContext::Variable {
539539+ prefix: var_prefix,
540540+ span,
541541+ });
542542+ }
543543+ } else {
544544+ // Variable completion context (no dot)
545545+ let var_prefix = if trimmed_prefix.len() > 1 {
546546+ trimmed_prefix[1..].to_string()
547547+ } else {
548548+ String::new()
549549+ };
550550+ return Some(CompletionContext::Variable {
551551+ prefix: var_prefix,
552552+ span,
553553+ });
554554+ }
555555+ }
556556+ _ if is_command_shape(input, shape, local_span) => {
557557+ let (full_prefix, full_span) =
558558+ build_command_prefix(input, shapes, idx, span, &prefix, global_offset);
559559+ return Some(CompletionContext::Command {
560560+ prefix: full_prefix,
561561+ span: full_span,
562562+ });
563563+ }
564564+ FlatShape::Block | FlatShape::Closure => {
565565+ if let Some(ctx) = handle_block_or_closure(
566566+ input,
567567+ shapes,
568568+ working_set,
569569+ &prefix,
570570+ span,
571571+ shape.as_str().trim_start_matches("shape_"),
572572+ idx,
573573+ local_span,
574574+ global_offset,
575575+ ) {
576576+ return Some(ctx);
577577+ }
578578+ }
579579+ FlatShape::Variable(var_id) => {
580580+ // Variable or cell path completion context
581581+ let trimmed_prefix = prefix.trim();
582582+ if trimmed_prefix.starts_with('$') {
583583+ // Check if this is a cell path (contains a dot after $)
584584+ if let Some((_, path_so_far, cell_prefix)) =
585585+ parse_cell_path(trimmed_prefix)
586586+ {
587587+ let prefix_byte_len = cell_prefix.len();
588588+ let cell_span_start = span.end.saturating_sub(prefix_byte_len);
589589+ return Some(CompletionContext::CellPath {
590590+ prefix: cell_prefix.to_string(),
591591+ span: Span::new(cell_span_start, span.end),
592592+ var_id: *var_id,
593593+ path_so_far: path_so_far
594594+ .iter()
595595+ .map(|s| s.to_string())
596596+ .collect(),
597597+ });
598598+ } else {
599599+ // Simple variable completion
600600+ let var_prefix = trimmed_prefix[1..].to_string();
601601+ return Some(CompletionContext::Variable {
602602+ prefix: var_prefix,
603603+ span,
604604+ });
605605+ }
606606+ } else {
607607+ // Fallback to argument context if no $ found
608608+ return Some(CompletionContext::Argument {
609609+ prefix: prefix.to_string(),
610610+ span,
611611+ });
612612+ }
613613+ }
614614+ _ => {
615615+ // Check if this is a variable or cell path (starts with $)
616616+ let trimmed_prefix = prefix.trim();
617617+ if trimmed_prefix.starts_with('$') {
618618+ // Check if this is a cell path (contains a dot after $)
619619+ if let Some((var_name, path_so_far, cell_prefix)) =
620620+ parse_cell_path(trimmed_prefix)
621621+ {
622622+ let var_id = lookup_variable_id(var_name, working_set);
623623+ if let Some(var_id) = var_id {
624624+ let prefix_byte_len = cell_prefix.len();
625625+ let cell_span_start = span.end.saturating_sub(prefix_byte_len);
626626+ return Some(CompletionContext::CellPath {
627627+ prefix: cell_prefix.to_string(),
628628+ span: Span::new(cell_span_start, span.end),
629629+ var_id,
630630+ path_so_far: path_so_far
631631+ .iter()
632632+ .map(|s| s.to_string())
633633+ .collect(),
634634+ });
635635+ } else {
636636+ let var_prefix = trimmed_prefix[1..].to_string();
637637+ return Some(CompletionContext::Variable {
638638+ prefix: var_prefix,
639639+ span,
640640+ });
641641+ }
642642+ } else {
643643+ // Simple variable completion
644644+ let var_prefix = if trimmed_prefix.len() > 1 {
645645+ trimmed_prefix[1..].to_string()
646646+ } else {
647647+ String::new()
648648+ };
649649+ return Some(CompletionContext::Variable {
650650+ prefix: var_prefix,
651651+ span,
652652+ });
653653+ }
654654+ } else {
655655+ // Use helper to determine flag or argument context
656656+ return Some(determine_flag_or_argument_context(
657657+ input,
658658+ shapes,
659659+ &trimmed_prefix,
660660+ idx,
661661+ local_span,
662662+ span,
663663+ global_offset,
664664+ ));
665665+ }
666666+ }
667667+ }
668668+ }
669669+ break;
670670+ }
671671+ }
672672+ None
673673+}
674674+675675+pub fn determine_context_fallback(
676676+ input: &str,
677677+ shapes: &[(Span, FlatShape)],
678678+ working_set: &StateWorkingSet,
679679+ engine_guard: &EngineState,
680680+ byte_pos: usize,
681681+ global_offset: usize,
682682+) -> Option<CompletionContext> {
683683+ use nu_parser::{TokenContents, lex};
684684+685685+ console_log!("[completion] Context is None, entering fallback logic");
686686+ // Check if there's a command-like shape before us
687687+ let mut has_separator_after_command = false;
688688+ for (span, shape) in shapes.iter().rev() {
689689+ let local_span = to_local_span(*span, global_offset);
690690+ if local_span.end <= byte_pos {
691691+ if is_command_shape(input, shape, local_span) {
692692+ // Check if there's a pipe or semicolon between this command and the cursor
693693+ has_separator_after_command =
694694+ has_separator_between(input, local_span.end, byte_pos);
695695+ console_log!(
696696+ "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
697697+ );
698698+ if !has_separator_after_command {
699699+ // Extract the command text
700700+ let cmd = safe_slice(input, local_span);
701701+ let cmd_name = extract_command_name(cmd).to_string();
702702+703703+ // Check if we're right after the command (only whitespace between command and cursor)
704704+ let text_after_command = if local_span.end < input.len() {
705705+ &input[local_span.end..byte_pos]
706706+ } else {
707707+ ""
708708+ };
709709+ let is_right_after_command = text_after_command.trim().is_empty();
710710+711711+ // If we're right after a command, check if it has positional arguments
712712+ if is_right_after_command {
713713+ if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
714714+ // Check if command has any positional arguments
715715+ let has_positional_args = !signature.required_positional.is_empty()
716716+ || !signature.optional_positional.is_empty();
717717+718718+ if has_positional_args {
719719+ // Count existing arguments before cursor
720720+ let mut arg_count = 0;
721721+ for (prev_span, prev_shape) in shapes.iter().rev() {
722722+ let prev_local_span = to_local_span(*prev_span, global_offset);
723723+ if prev_local_span.end <= byte_pos
724724+ && prev_local_span.end > local_span.end
725725+ {
726726+ if !is_command_shape(input, prev_shape, prev_local_span) {
727727+ let arg_text = safe_slice(input, prev_local_span);
728728+ let trimmed_arg = arg_text.trim();
729729+ // Don't count flags (starting with -) or empty arguments
730730+ if !trimmed_arg.is_empty()
731731+ && !trimmed_arg.starts_with('-')
732732+ {
733733+ arg_count += 1;
734734+ }
735735+ }
736736+ }
737737+ }
738738+739739+ console_log!(
740740+ "[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}"
741741+ );
742742+743743+ return Some(CompletionContext::CommandArgument {
744744+ prefix: String::new(),
745745+ span: Span::new(byte_pos, byte_pos),
746746+ command_name: cmd_name,
747747+ arg_index: arg_count,
748748+ });
749749+ } else {
750750+ // No positional arguments, don't show any completions
751751+ console_log!(
752752+ "[completion] Command {cmd_name:?} has no positional args, not showing completions"
753753+ );
754754+ // Leave context as None to show no completions
755755+ return None;
756756+ }
757757+ } else {
758758+ // Couldn't find signature, don't show completions
759759+ console_log!(
760760+ "[completion] Could not find signature for {cmd_name:?}, not showing completions"
761761+ );
762762+ // Leave context as None to show no completions
763763+ return None;
764764+ }
765765+ } else {
766766+ // Not right after command, complete the command itself
767767+ console_log!("[completion] Set Command context with prefix: {cmd:?}");
768768+ return Some(CompletionContext::Command {
769769+ prefix: cmd.to_string(),
770770+ span: local_span,
771771+ });
772772+ }
773773+ }
774774+ }
775775+ break;
776776+ }
777777+ }
778778+779779+ // No command found before, check context from tokens
780780+ console_log!("[completion] No command found before cursor, checking tokens");
781781+ // No command before, check context from tokens
782782+ let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
783783+ let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
784784+785785+ let is_cmd_context = if let Some(token) = last_token {
786786+ let matches = matches!(
787787+ token.contents,
788788+ TokenContents::Pipe
789789+ | TokenContents::PipePipe
790790+ | TokenContents::Semicolon
791791+ | TokenContents::Eol
792792+ );
793793+ console_log!(
794794+ "[completion] Last token: {contents:?}, is_cmd_context from token={matches}",
795795+ contents = token.contents
796796+ );
797797+ matches
798798+ } else {
799799+ console_log!(
800800+ "[completion] No last token found, assuming start of input (is_cmd_context=true)"
801801+ );
802802+ true // Start of input
803803+ };
804804+805805+ // Look for the last non-whitespace token before cursor
806806+ let text_before = &input[..byte_pos];
807807+808808+ // Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
809809+ let text_before_trimmed = text_before.trim_end();
810810+ let is_inside_block = text_before_trimmed.ends_with('{');
811811+ // If we found a separator after a command, we're starting a new command
812812+ let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
813813+ console_log!(
814814+ "[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}"
815815+ );
816816+817817+ // Find the last word before cursor
818818+ let last_word_start = text_before
819819+ .rfind(|c: char| c.is_whitespace() || is_separator_char(c))
820820+ .map(|i| i + 1)
821821+ .unwrap_or(0);
822822+823823+ let last_word = text_before[last_word_start..].trim_start();
824824+ console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}");
825825+826826+ if is_cmd_context {
827827+ Some(CompletionContext::Command {
828828+ prefix: last_word.to_string(),
829829+ span: Span::new(last_word_start, byte_pos),
830830+ })
831831+ } else {
832832+ // Check if this is a variable or cell path (starts with $)
833833+ let trimmed_word = last_word.trim();
834834+ if trimmed_word.starts_with('$') {
835835+ // Check if this is a cell path (contains a dot after $)
836836+ if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) {
837837+ let var_id = lookup_variable_id(&var_name, working_set);
838838+839839+ if let Some(var_id) = var_id {
840840+ let prefix_byte_len = cell_prefix.len();
841841+ let cell_span_start = byte_pos.saturating_sub(prefix_byte_len);
842842+ Some(CompletionContext::CellPath {
843843+ prefix: cell_prefix.to_string(),
844844+ span: Span::new(cell_span_start, byte_pos),
845845+ var_id,
846846+ path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
847847+ })
848848+ } else {
849849+ let var_prefix = trimmed_word[1..].to_string();
850850+ Some(CompletionContext::Variable {
851851+ prefix: var_prefix,
852852+ span: Span::new(last_word_start, byte_pos),
853853+ })
854854+ }
855855+ } else {
856856+ // Simple variable completion
857857+ let var_prefix = trimmed_word[1..].to_string();
858858+ Some(CompletionContext::Variable {
859859+ prefix: var_prefix,
860860+ span: Span::new(last_word_start, byte_pos),
861861+ })
862862+ }
863863+ } else if trimmed_word.starts_with('-') {
864864+ // Try to find command by looking backwards through shapes
865865+ let mut found_cmd = None;
866866+ for (span, shape) in shapes.iter().rev() {
867867+ let local_span = to_local_span(*span, global_offset);
868868+ if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) {
869869+ let cmd_text = safe_slice(input, local_span);
870870+ let cmd_name = extract_command_name(cmd_text).to_string();
871871+ found_cmd = Some(cmd_name);
872872+ break;
873873+ }
874874+ }
875875+ if let Some(cmd_name) = found_cmd {
876876+ Some(CompletionContext::Flag {
877877+ prefix: trimmed_word.to_string(),
878878+ span: Span::new(last_word_start, byte_pos),
879879+ command_name: cmd_name,
880880+ })
881881+ } else {
882882+ Some(CompletionContext::Argument {
883883+ prefix: last_word.to_string(),
884884+ span: Span::new(last_word_start, byte_pos),
885885+ })
886886+ }
887887+ } else {
888888+ // Try to find command and argument index
889889+ let mut found_cmd = None;
890890+ let mut arg_count = 0;
891891+ for (span, shape) in shapes.iter().rev() {
892892+ let local_span = to_local_span(*span, global_offset);
893893+ if local_span.end <= byte_pos {
894894+ if is_command_shape(input, shape, local_span) {
895895+ let cmd_text = safe_slice(input, local_span);
896896+ let cmd_name = extract_command_name(cmd_text).to_string();
897897+ found_cmd = Some(cmd_name);
898898+ break;
899899+ } else {
900900+ let arg_text = safe_slice(input, local_span);
901901+ let trimmed_arg = arg_text.trim();
902902+ if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
903903+ arg_count += 1;
904904+ }
905905+ }
906906+ }
907907+ }
908908+ if let Some(cmd_name) = found_cmd {
909909+ Some(CompletionContext::CommandArgument {
910910+ prefix: trimmed_word.to_string(),
911911+ span: Span::new(last_word_start, byte_pos),
912912+ command_name: cmd_name,
913913+ arg_index: arg_count,
914914+ })
915915+ } else {
916916+ Some(CompletionContext::Argument {
917917+ prefix: last_word.to_string(),
918918+ span: Span::new(last_word_start, byte_pos),
919919+ })
920920+ }
921921+ }
922922+ }
923923+}
924924+925925+pub fn determine_context(
926926+ input: &str,
927927+ shapes: &[(Span, FlatShape)],
928928+ working_set: &StateWorkingSet,
929929+ engine_guard: &EngineState,
930930+ byte_pos: usize,
931931+ global_offset: usize,
932932+) -> Option<CompletionContext> {
933933+ // First try to determine context from shapes
934934+ if let Some(ctx) =
935935+ determine_context_from_shape(input, shapes, working_set, byte_pos, global_offset)
936936+ {
937937+ return Some(ctx);
938938+ }
939939+940940+ // Fallback to token-based context determination
941941+ determine_context_fallback(
942942+ input,
943943+ shapes,
944944+ working_set,
945945+ engine_guard,
946946+ byte_pos,
947947+ global_offset,
948948+ )
949949+}
+51
src/completion/files.rs
···11+use crate::completion::helpers::to_char_span;
22+use crate::completion::types::Suggestion;
33+use nu_protocol::Span;
44+55+pub fn generate_file_suggestions(
66+ prefix: &str,
77+ span: Span,
88+ root: &std::sync::Arc<vfs::VfsPath>,
99+ description: Option<String>,
1010+ input: &str,
1111+) -> Vec<Suggestion> {
1212+ let (dir, file_prefix) = prefix
1313+ .rfind('/')
1414+ .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
1515+ .unwrap_or(("", prefix));
1616+1717+ let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
1818+ .then(|| &dir[..dir.len() - 1])
1919+ .unwrap_or(dir);
2020+2121+ let target_dir = if !dir.is_empty() {
2222+ match root.join(dir_to_join) {
2323+ Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
2424+ _ => None,
2525+ }
2626+ } else {
2727+ Some(root.join("").unwrap())
2828+ };
2929+3030+ let mut file_suggestions = Vec::new();
3131+ if let Some(d) = target_dir {
3232+ if let Ok(iterator) = d.read_dir() {
3333+ let char_span = to_char_span(input, span);
3434+ for entry in iterator {
3535+ let name = entry.filename();
3636+ if name.starts_with(file_prefix) {
3737+ let full_completion = format!("{}{}", dir, name);
3838+ file_suggestions.push(Suggestion {
3939+ name: full_completion.clone(),
4040+ description: description.clone(),
4141+ is_command: false,
4242+ rendered: full_completion,
4343+ span_start: char_span.start,
4444+ span_end: char_span.end,
4545+ });
4646+ }
4747+ }
4848+ }
4949+ }
5050+ file_suggestions
5151+}
···11+use crate::console_log;
22+use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
33+use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span, Value};
44+use std::collections::HashMap;
55+66+pub fn eval_variable_for_completion(
77+ var_id: nu_protocol::VarId,
88+ working_set: &StateWorkingSet,
99+ engine_guard: &EngineState,
1010+ stack_guard: &Stack,
1111+) -> Option<Value> {
1212+ match var_id {
1313+ id if id == NU_VARIABLE_ID => {
1414+ // $nu - get from engine state constant
1515+ engine_guard.get_constant(id).cloned()
1616+ }
1717+ id if id == ENV_VARIABLE_ID => {
1818+ // $env - build from environment variables in engine state
1919+ // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars)
2020+ let mut pairs: Vec<(String, Value)> = Vec::new();
2121+ for overlay_env in engine_guard.env_vars.values() {
2222+ for (name, value) in overlay_env.iter() {
2323+ pairs.push((name.clone(), value.clone()));
2424+ }
2525+ }
2626+ pairs.sort_by(|a, b| a.0.cmp(&b.0));
2727+ // Deduplicate by name (later overlays override earlier ones)
2828+ pairs.dedup_by(|a, b| a.0 == b.0);
2929+ Some(Value::record(pairs.into_iter().collect(), Span::unknown()))
3030+ }
3131+ id if id == IN_VARIABLE_ID => {
3232+ // $in - typically not available at completion time
3333+ None
3434+ }
3535+ _ => {
3636+ // User-defined variable - try to get const value first
3737+ let var_info = working_set.get_variable(var_id);
3838+ if let Some(const_val) = &var_info.const_val {
3939+ Some(const_val.clone())
4040+ } else {
4141+ // Variable doesn't have a const value (runtime value)
4242+ // Try to get the value from the stack (runtime storage)
4343+ match stack_guard.get_var(var_id, Span::unknown()) {
4444+ Ok(value) => {
4545+ console_log!("[completion] Found variable {var_id:?} value in stack");
4646+ Some(value)
4747+ }
4848+ Err(_) => {
4949+ // Variable not in stack either
5050+ console_log!(
5151+ "[completion] Variable {var_id:?} has no const value and not in stack, type: {ty:?}",
5252+ ty = var_info.ty
5353+ );
5454+ None
5555+ }
5656+ }
5757+ }
5858+ }
5959+ }
6060+}
6161+6262+pub fn get_columns_from_value(value: &Value) -> Vec<(String, Option<String>)> {
6363+ match value {
6464+ Value::Record { val, .. } => val
6565+ .iter()
6666+ .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
6767+ .collect(),
6868+ Value::List { vals, .. } => {
6969+ // Get common columns from list of records
7070+ if let Some(first) = vals.first() {
7171+ if let Value::Record { val, .. } = first {
7272+ return val
7373+ .iter()
7474+ .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
7575+ .collect();
7676+ }
7777+ }
7878+ vec![]
7979+ }
8080+ _ => vec![],
8181+ }
8282+}
8383+8484+pub fn follow_cell_path(value: &Value, path: &[&str]) -> Option<Value> {
8585+ let mut current = value.clone();
8686+ for member in path {
8787+ match ¤t {
8888+ Value::Record { val, .. } => {
8989+ current = val.get(member)?.clone();
9090+ }
9191+ Value::List { vals, .. } => {
9292+ // Try to parse as index or get from first record
9393+ if let Ok(idx) = member.parse::<usize>() {
9494+ current = vals.get(idx)?.clone();
9595+ } else if let Some(first) = vals.first() {
9696+ if let Value::Record { val, .. } = first {
9797+ current = val.get(member)?.clone();
9898+ } else {
9999+ return None;
100100+ }
101101+ } else {
102102+ return None;
103103+ }
104104+ }
105105+ _ => return None,
106106+ }
107107+ }
108108+ Some(current)
109109+}
110110+111111+pub fn extract_closure_params(input: &str, cursor_pos: usize) -> Vec<String> {
112112+ let mut params = Vec::new();
113113+114114+ // Find all closures in the input by looking for {|...| patterns
115115+ // We need to find closures that contain the cursor position
116116+ let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions
117117+ let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params)
118118+119119+ let mut i = 0;
120120+ let chars: Vec<char> = input.chars().collect();
121121+122122+ while i < chars.len() {
123123+ if chars[i] == '{' {
124124+ brace_stack.push(i);
125125+ } else if chars[i] == '}' {
126126+ if let Some(start) = brace_stack.pop() {
127127+ // Check if this is a closure with parameters: {|param| ...}
128128+ if start + 1 < chars.len() && chars[start + 1] == '|' {
129129+ // Find the parameter list
130130+ let param_start = start + 2;
131131+ let mut param_end = param_start;
132132+133133+ // Find the closing | of the parameter list
134134+ while param_end < chars.len() && chars[param_end] != '|' {
135135+ param_end += 1;
136136+ }
137137+138138+ if param_end < chars.len() {
139139+ // Extract parameter names
140140+ let params_text: String = chars[param_start..param_end].iter().collect();
141141+ let param_names: Vec<String> = params_text
142142+ .split(',')
143143+ .map(|s| s.trim().to_string())
144144+ .filter(|s| !s.is_empty())
145145+ .collect();
146146+147147+ closures.push((start, i + 1, param_names));
148148+ }
149149+ }
150150+ }
151151+ }
152152+ i += 1;
153153+ }
154154+155155+ // Find closures that contain the cursor position
156156+ // A closure contains the cursor if: start <= cursor_pos < end
157157+ for (start, end, param_names) in closures {
158158+ if start <= cursor_pos && cursor_pos < end {
159159+ console_log!(
160160+ "[completion] Found closure at [{start}, {end}) containing cursor {cursor_pos}, params: {param_names:?}"
161161+ );
162162+ params.extend(param_names);
163163+ }
164164+ }
165165+166166+ params
167167+}
168168+169169+pub fn collect_variables(
170170+ working_set: &StateWorkingSet,
171171+ input: &str,
172172+ cursor_pos: usize,
173173+) -> HashMap<String, nu_protocol::VarId> {
174174+ let mut variables = HashMap::new();
175175+176176+ // Add built-in variables
177177+ variables.insert("$nu".to_string(), NU_VARIABLE_ID);
178178+ variables.insert("$in".to_string(), IN_VARIABLE_ID);
179179+ variables.insert("$env".to_string(), ENV_VARIABLE_ID);
180180+181181+ // Collect closure parameters at cursor position
182182+ // We don't need real var_ids for closure parameters since they're not evaluated yet
183183+ // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder)
184184+ // The actual var_id lookup will happen when the variable is used
185185+ let closure_params = extract_closure_params(input, cursor_pos);
186186+ for param_name in closure_params {
187187+ let var_name = format!("${}", param_name);
188188+ // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name
189189+ // The completion logic only needs the name, not the actual var_id
190190+ variables.insert(var_name.clone(), IN_VARIABLE_ID);
191191+ console_log!("[completion] Added closure parameter: {var_name:?}");
192192+ }
193193+194194+ // Collect from working set delta scope
195195+ let mut removed_overlays = vec![];
196196+ for scope_frame in working_set.delta.scope.iter().rev() {
197197+ for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
198198+ for (name, var_id) in &overlay_frame.vars {
199199+ let name = String::from_utf8_lossy(name).to_string();
200200+ variables.insert(name, *var_id);
201201+ }
202202+ }
203203+ }
204204+205205+ // Collect from permanent state scope
206206+ for overlay_frame in working_set
207207+ .permanent_state
208208+ .active_overlays(&removed_overlays)
209209+ .rev()
210210+ {
211211+ for (name, var_id) in &overlay_frame.vars {
212212+ let name = String::from_utf8_lossy(name).to_string();
213213+ variables.insert(name, *var_id);
214214+ }
215215+ }
216216+217217+ variables
218218+}