1use crate::completion::helpers::*;
2use crate::completion::types::{CompletionContext, CompletionKind};
3use crate::console_log;
4use nu_parser::FlatShape;
5use nu_protocol::engine::{EngineState, StateWorkingSet};
6use nu_protocol::{Signature, Span};
7
8pub fn find_command_and_arg_index(
9 input: &str,
10 shapes: &[(Span, FlatShape)],
11 current_idx: usize,
12 current_local_span: Span,
13 global_offset: usize,
14) -> Option<(String, usize)> {
15 let mut command_name: Option<String> = None;
16 let mut arg_count = 0;
17
18 // Look backwards through shapes to find the command
19 for i in (0..current_idx).rev() {
20 if let Some((prev_span, prev_shape)) = shapes.get(i) {
21 let prev_local_span = to_local_span(*prev_span, global_offset);
22
23 // Check if there's a separator between this shape and the next one
24 let next_shape_start = if i + 1 < shapes.len() {
25 to_local_span(shapes[i + 1].0, global_offset).start
26 } else {
27 current_local_span.start
28 };
29
30 if has_separator_between(input, prev_local_span.end, next_shape_start) {
31 break; // Stop at separator
32 }
33
34 if is_command_shape(input, prev_shape, prev_local_span) {
35 // Found the command
36 let cmd_text = safe_slice(input, prev_local_span);
37 let cmd_name = extract_command_name(cmd_text);
38 command_name = Some(cmd_name.to_string());
39 break;
40 } else {
41 // This is an argument - count it if it's not a flag
42 let arg_text = safe_slice(input, prev_local_span);
43 let trimmed_arg = arg_text.trim();
44 // Don't count flags (starting with -) or empty arguments
45 if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
46 arg_count += 1;
47 }
48 }
49 }
50 }
51
52 command_name.map(|name| (name, arg_count))
53}
54
55pub fn build_command_prefix(
56 input: &str,
57 shapes: &[(Span, FlatShape)],
58 current_idx: usize,
59 current_local_span: Span,
60 current_prefix: &str,
61 global_offset: usize,
62) -> (String, Span) {
63 let mut span_start = current_local_span.start;
64
65 // Look backwards through shapes to find previous command words
66 for i in (0..current_idx).rev() {
67 if let Some((prev_span, prev_shape)) = shapes.get(i) {
68 let prev_local_span = to_local_span(*prev_span, global_offset);
69
70 if is_command_shape(input, prev_shape, prev_local_span) {
71 // Check if there's a separator between this shape and the next one
72 let next_shape_start = if i + 1 < shapes.len() {
73 to_local_span(shapes[i + 1].0, global_offset).start
74 } else {
75 current_local_span.start
76 };
77
78 // Check if there's a separator (pipe, semicolon, etc.) between shapes
79 // Whitespace is fine, but separators indicate a new command
80 if has_separator_between(input, prev_local_span.end, next_shape_start) {
81 break; // Stop at separator
82 }
83
84 // Update span start to include this command word
85 span_start = prev_local_span.start;
86 } else {
87 // Not a command shape, stop looking backwards
88 break;
89 }
90 }
91 }
92
93 // Extract the full prefix from the input, preserving exact spacing
94 let span_end = current_local_span.end;
95 let full_prefix = if span_start < input.len() {
96 safe_slice(input, Span::new(span_start, span_end)).to_string()
97 } else {
98 current_prefix.to_string()
99 };
100
101 (full_prefix, Span::new(span_start, span_end))
102}
103
104pub fn get_command_signature(engine_guard: &EngineState, cmd_name: &str) -> Option<Signature> {
105 engine_guard
106 .find_decl(cmd_name.as_bytes(), &[])
107 .map(|id| engine_guard.get_decl(id).signature())
108}
109
110/// Creates CommandArgument context(s), and optionally adds a Command context for subcommands
111/// if we're at argument index 0 and the command has subcommands.
112pub fn create_command_argument_contexts(
113 command_name: String,
114 arg_index: usize,
115 prefix: String,
116 span: Span,
117 working_set: &StateWorkingSet,
118 _engine_guard: &EngineState,
119) -> Vec<CompletionContext> {
120 let mut contexts = Vec::new();
121
122 // Always add the CommandArgument context
123 contexts.push(CompletionContext {
124 kind: CompletionKind::CommandArgument {
125 command_name: command_name.clone(),
126 arg_index,
127 },
128 prefix: prefix.clone(),
129 span,
130 });
131
132 // If we're at argument index 0, check if the command has subcommands
133 if arg_index == 0 {
134 // Check if command has subcommands
135 // Subcommands are commands that start with "command_name " (with space)
136 let parent_prefix = format!("{} ", command_name);
137 let subcommands = working_set
138 .find_commands_by_predicate(|value| value.starts_with(parent_prefix.as_bytes()), true);
139
140 if !subcommands.is_empty() {
141 // Command has subcommands - add a Command context for subcommands
142 console_log!(
143 "[completion] Command {command_name:?} has subcommands, adding Command context for subcommands"
144 );
145 contexts.push(CompletionContext {
146 kind: CompletionKind::Command {
147 parent_command: Some(command_name),
148 },
149 prefix,
150 span,
151 });
152 }
153 }
154
155 contexts
156}
157
158pub fn determine_flag_or_argument_context(
159 input: &str,
160 shapes: &[(Span, FlatShape)],
161 prefix: &str,
162 idx: usize,
163 local_span: Span,
164 span: Span,
165 global_offset: usize,
166 working_set: &StateWorkingSet,
167 _engine_guard: &EngineState,
168) -> Vec<CompletionContext> {
169 let trimmed_prefix = prefix.trim();
170 if trimmed_prefix.starts_with('-') {
171 // This looks like a flag - find the command
172 if let Some((cmd_name, _)) =
173 find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
174 {
175 vec![CompletionContext {
176 kind: CompletionKind::Flag {
177 command_name: cmd_name,
178 },
179 prefix: trimmed_prefix.to_string(),
180 span,
181 }]
182 } else {
183 vec![CompletionContext {
184 kind: CompletionKind::Argument,
185 prefix: prefix.to_string(),
186 span,
187 }]
188 }
189 } else {
190 // This is a positional argument - find the command and argument index
191 if let Some((cmd_name, arg_index)) =
192 find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
193 {
194 create_command_argument_contexts(
195 cmd_name,
196 arg_index,
197 trimmed_prefix.to_string(),
198 span,
199 working_set,
200 _engine_guard,
201 )
202 } else {
203 vec![CompletionContext {
204 kind: CompletionKind::Argument,
205 prefix: prefix.to_string(),
206 span,
207 }]
208 }
209 }
210}
211
212pub fn handle_block_or_closure(
213 input: &str,
214 shapes: &[(Span, FlatShape)],
215 working_set: &StateWorkingSet,
216 engine_guard: &EngineState,
217 prefix: &str,
218 span: Span,
219 shape_name: &str,
220 current_idx: usize,
221 local_span: Span,
222 global_offset: usize,
223) -> Vec<CompletionContext> {
224 console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}");
225
226 // Check if the content ends with a pipe or semicolon
227 let prefix_ends_with_separator = ends_with_separator(prefix);
228 let last_sep_pos_in_prefix = if prefix_ends_with_separator {
229 find_last_separator_pos(prefix)
230 } else {
231 None
232 };
233 console_log!(
234 "[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}"
235 );
236
237 if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
238 console_log!(
239 "[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}"
240 );
241
242 if is_empty {
243 // Empty block/closure or just whitespace
244 // Check if there's a command shape before this closure/block shape
245 // If so, we might be completing after that command
246 let mut found_command: Option<String> = None;
247 for i in (0..current_idx).rev() {
248 if let Some((prev_span, prev_shape)) = shapes.get(i) {
249 let prev_local_span = to_local_span(*prev_span, global_offset);
250 // Check if this shape is before the current closure and is a command
251 if prev_local_span.end <= local_span.start {
252 if is_command_shape(input, prev_shape, prev_local_span) {
253 let cmd_text = safe_slice(input, prev_local_span);
254 let cmd_full = cmd_text.trim().to_string();
255
256 // Extract the full command text - if it contains spaces, it might be a subcommand
257 // We'll use the first word for parent_command to show subcommands
258 // The suggestion generator will filter appropriately
259 let cmd_first_word = extract_command_name(cmd_text).to_string();
260
261 // If the command contains spaces, it's likely a full command (subcommand)
262 // In that case, we shouldn't show subcommands
263 if cmd_full.contains(' ') && cmd_full != cmd_first_word {
264 // It's a full command (subcommand), don't show subcommands
265 console_log!(
266 "[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions"
267 );
268 return Vec::new();
269 }
270
271 // Use the first word to show subcommands
272 found_command = Some(cmd_first_word);
273 console_log!(
274 "[completion] {shape_name} is empty but found command {found_command:?} before it"
275 );
276 break;
277 }
278 }
279 }
280 }
281
282 if let Some(cmd_name) = found_command {
283 // We found a command before the closure, show subcommands of that command
284 console_log!(
285 "[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}"
286 );
287 vec![CompletionContext {
288 kind: CompletionKind::Command {
289 parent_command: Some(cmd_name),
290 },
291 prefix: String::new(),
292 span: adjusted_span,
293 }]
294 } else {
295 // Truly empty - show all commands
296 console_log!("[completion] {shape_name} is empty, setting Command context");
297 vec![CompletionContext {
298 kind: CompletionKind::Command {
299 parent_command: None,
300 },
301 prefix: String::new(),
302 span: adjusted_span,
303 }]
304 }
305 } else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
306 // After a separator - command context
307 let after_sep = prefix[last_sep_pos..].trim_start();
308 console_log!(
309 "[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context"
310 );
311 vec![CompletionContext {
312 kind: CompletionKind::Command {
313 parent_command: None,
314 },
315 prefix: after_sep.to_string(),
316 span: Span::new(span.start + last_sep_pos, span.end),
317 }]
318 } else {
319 console_log!(
320 "[completion] {shape_name} has no separator, checking for variable/flag/argument context"
321 );
322 // Check if this is a variable or cell path first
323 let trimmed = trimmed_prefix.trim();
324
325 if trimmed.starts_with('$') {
326 // Variable or cell path completion
327 if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) {
328 let var_id = lookup_variable_id(var_name, working_set);
329
330 if let Some(var_id) = var_id {
331 let prefix_byte_len = cell_prefix.len();
332 let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len);
333 console_log!(
334 "[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}"
335 );
336 vec![CompletionContext {
337 kind: CompletionKind::CellPath {
338 var_id,
339 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
340 },
341 prefix: cell_prefix.to_string(),
342 span: Span::new(cell_span_start, adjusted_span.end),
343 }]
344 } else {
345 // Unknown variable, fall back to variable completion
346 let var_prefix = trimmed[1..].to_string();
347 console_log!(
348 "[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}"
349 );
350 vec![CompletionContext {
351 kind: CompletionKind::Variable,
352 prefix: var_prefix,
353 span: adjusted_span,
354 }]
355 }
356 } else {
357 // Simple variable completion (no dot)
358 let var_prefix = if trimmed.len() > 1 {
359 trimmed[1..].to_string()
360 } else {
361 String::new()
362 };
363 console_log!(
364 "[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}"
365 );
366 vec![CompletionContext {
367 kind: CompletionKind::Variable,
368 prefix: var_prefix,
369 span: adjusted_span,
370 }]
371 }
372 } else if trimmed.starts_with('-') {
373 // Flag completion
374 if let Some((cmd_name, _)) = find_command_and_arg_index(
375 input,
376 shapes,
377 current_idx,
378 local_span,
379 global_offset,
380 ) {
381 console_log!(
382 "[completion] {shape_name}: Found command {cmd_name:?} for flag completion"
383 );
384 vec![CompletionContext {
385 kind: CompletionKind::Flag {
386 command_name: cmd_name,
387 },
388 prefix: trimmed.to_string(),
389 span: adjusted_span,
390 }]
391 } else {
392 vec![CompletionContext {
393 kind: CompletionKind::Argument,
394 prefix: trimmed_prefix.to_string(),
395 span: adjusted_span,
396 }]
397 }
398 } else {
399 // Try to find the command and argument index
400 if let Some((cmd_name, arg_index)) = find_command_and_arg_index(
401 input,
402 shapes,
403 current_idx,
404 local_span,
405 global_offset,
406 ) {
407 console_log!(
408 "[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion"
409 );
410 create_command_argument_contexts(
411 cmd_name,
412 arg_index,
413 trimmed.to_string(),
414 adjusted_span,
415 working_set,
416 engine_guard,
417 )
418 } else {
419 // No command found, treat as regular argument
420 console_log!(
421 "[completion] {shape_name}: No command found, using Argument context"
422 );
423 vec![CompletionContext {
424 kind: CompletionKind::Argument,
425 prefix: trimmed_prefix.to_string(),
426 span: adjusted_span,
427 }]
428 }
429 }
430 }
431 } else {
432 Vec::new()
433 }
434}
435
436pub fn handle_variable_string_shape(
437 input: &str,
438 shapes: &[(Span, FlatShape)],
439 working_set: &StateWorkingSet,
440 engine_guard: &EngineState,
441 idx: usize,
442 prefix: &str,
443 span: Span,
444 local_span: Span,
445 global_offset: usize,
446) -> Vec<CompletionContext> {
447 if idx == 0 {
448 return Vec::new();
449 }
450
451 let prev_shape = &shapes[idx - 1];
452 let prev_local_span = to_local_span(prev_shape.0, global_offset);
453
454 if let FlatShape::Variable(var_id) = prev_shape.1 {
455 // Check if the variable shape ends right where this shape starts (or very close)
456 // Allow for a small gap (like a dot) between shapes
457 let gap = local_span.start.saturating_sub(prev_local_span.end);
458 if gap <= 1 {
459 // This is a cell path - the String shape contains the field name(s)
460 // The prefix might be like "na" or "field.subfield"
461 let trimmed_prefix = prefix.trim();
462 let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix);
463
464 let prefix_byte_len = cell_prefix.len();
465 let cell_span_start = span.end.saturating_sub(prefix_byte_len);
466 console_log!(
467 "[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}"
468 );
469 vec![CompletionContext {
470 kind: CompletionKind::CellPath {
471 var_id,
472 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
473 },
474 prefix: cell_prefix.to_string(),
475 span: Span::new(cell_span_start, span.end),
476 }]
477 } else {
478 // Gap between shapes, use helper to determine context
479 determine_flag_or_argument_context(
480 input,
481 shapes,
482 &prefix.trim(),
483 idx,
484 local_span,
485 span,
486 global_offset,
487 working_set,
488 engine_guard,
489 )
490 }
491 } else {
492 // Previous shape is not a Variable, use helper to determine context
493 determine_flag_or_argument_context(
494 input,
495 shapes,
496 &prefix.trim(),
497 idx,
498 local_span,
499 span,
500 global_offset,
501 working_set,
502 engine_guard,
503 )
504 }
505}
506
507pub fn handle_dot_shape(
508 _input: &str,
509 shapes: &[(Span, FlatShape)],
510 idx: usize,
511 prefix: &str,
512 span: Span,
513 local_span: Span,
514 global_offset: usize,
515) -> Vec<CompletionContext> {
516 if idx == 0 {
517 return vec![CompletionContext {
518 kind: CompletionKind::Argument,
519 prefix: prefix.to_string(),
520 span,
521 }];
522 }
523
524 let prev_shape = &shapes[idx - 1];
525 let prev_local_span = to_local_span(prev_shape.0, global_offset);
526
527 if let FlatShape::Variable(var_id) = prev_shape.1 {
528 // Check if the variable shape ends right where this shape starts
529 if prev_local_span.end == local_span.start {
530 let trimmed_prefix = prefix.trim();
531 // Parse path members from the prefix (which is like ".field" or ".field.subfield")
532 let after_dot = &trimmed_prefix[1..]; // Remove leading dot
533 let (path_so_far, cell_prefix) = if after_dot.is_empty() {
534 (vec![], "")
535 } else {
536 parse_cell_path_from_fields(after_dot)
537 };
538
539 let prefix_byte_len = cell_prefix.len();
540 let cell_span_start = span.end.saturating_sub(prefix_byte_len);
541 console_log!(
542 "[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}"
543 );
544 vec![CompletionContext {
545 kind: CompletionKind::CellPath {
546 var_id,
547 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
548 },
549 prefix: cell_prefix.to_string(),
550 span: Span::new(cell_span_start, span.end),
551 }]
552 } else {
553 // Gap between shapes, fall through to default handling
554 vec![CompletionContext {
555 kind: CompletionKind::Argument,
556 prefix: prefix.to_string(),
557 span,
558 }]
559 }
560 } else {
561 // Previous shape is not a Variable, this is likely a file path starting with .
562 vec![CompletionContext {
563 kind: CompletionKind::Argument,
564 prefix: prefix.to_string(),
565 span,
566 }]
567 }
568}
569
570pub fn determine_context_from_shape(
571 input: &str,
572 shapes: &[(Span, FlatShape)],
573 working_set: &StateWorkingSet,
574 engine_guard: &EngineState,
575 byte_pos: usize,
576 global_offset: usize,
577) -> Vec<CompletionContext> {
578 // First, check if cursor is within a shape
579 for (idx, (span, shape)) in shapes.iter().enumerate() {
580 let local_span = to_local_span(*span, global_offset);
581
582 if local_span.start <= byte_pos && byte_pos <= local_span.end {
583 console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}");
584
585 // Check if there's a pipe or semicolon between this shape's end and the cursor
586 // If so, we're starting a new command and should ignore this shape
587 let has_sep = has_separator_between(input, local_span.end, byte_pos);
588 if has_sep {
589 console_log!(
590 "[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape",
591 end = local_span.end
592 );
593 // There's a separator, so we're starting a new command - skip this shape
594 continue;
595 }
596
597 let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
598 let prefix = safe_slice(input, span);
599 console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}");
600
601 // Special case: if prefix is just '{' (possibly with whitespace),
602 // we're at the start of a block and should complete commands
603 let trimmed_prefix = prefix.trim();
604 if trimmed_prefix == "{" {
605 // We're right after '{' - command context
606 if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
607 return vec![CompletionContext {
608 kind: CompletionKind::Command {
609 parent_command: None,
610 },
611 prefix: String::new(),
612 span: adjusted_span,
613 }];
614 }
615 } else {
616 match shape {
617 // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes
618 _ if { idx > 0 && matches!(shape, FlatShape::String) } => {
619 let contexts = handle_variable_string_shape(
620 input,
621 shapes,
622 working_set,
623 engine_guard,
624 idx,
625 &prefix,
626 span,
627 local_span,
628 global_offset,
629 );
630 if !contexts.is_empty() {
631 return contexts;
632 }
633 }
634 // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes
635 _ if {
636 let trimmed_prefix = prefix.trim();
637 trimmed_prefix.starts_with('.') && idx > 0
638 } =>
639 {
640 let contexts = handle_dot_shape(
641 input,
642 shapes,
643 idx,
644 &prefix,
645 span,
646 local_span,
647 global_offset,
648 );
649 if !contexts.is_empty() {
650 return contexts;
651 }
652 }
653 _ if {
654 // Check if this is a variable or cell path (starts with $) before treating as command
655 let trimmed_prefix = prefix.trim();
656 trimmed_prefix.starts_with('$')
657 } =>
658 {
659 let trimmed_prefix = prefix.trim();
660 // Check if this is a cell path (contains a dot after $)
661 if let Some((var_name, path_so_far, cell_prefix)) =
662 parse_cell_path(trimmed_prefix)
663 {
664 // Find the variable ID
665 let var_id = lookup_variable_id(var_name, working_set);
666
667 if let Some(var_id) = var_id {
668 // Calculate span for the cell path member being completed
669 let prefix_byte_len = cell_prefix.len();
670 let cell_span_start = span.end.saturating_sub(prefix_byte_len);
671 return vec![CompletionContext {
672 kind: CompletionKind::CellPath {
673 var_id,
674 path_so_far: path_so_far
675 .iter()
676 .map(|s| s.to_string())
677 .collect(),
678 },
679 prefix: cell_prefix.to_string(),
680 span: Span::new(cell_span_start, span.end),
681 }];
682 } else {
683 // Unknown variable, fall back to variable completion
684 let var_prefix = trimmed_prefix[1..].to_string();
685 return vec![CompletionContext {
686 kind: CompletionKind::Variable,
687 prefix: var_prefix,
688 span,
689 }];
690 }
691 } else {
692 // Variable completion context (no dot)
693 let var_prefix = if trimmed_prefix.len() > 1 {
694 trimmed_prefix[1..].to_string()
695 } else {
696 String::new()
697 };
698 return vec![CompletionContext {
699 kind: CompletionKind::Variable,
700 prefix: var_prefix,
701 span,
702 }];
703 }
704 }
705 _ if is_command_shape(input, shape, local_span) => {
706 let (full_prefix, full_span) =
707 build_command_prefix(input, shapes, idx, span, &prefix, global_offset);
708 return vec![CompletionContext {
709 kind: CompletionKind::Command {
710 parent_command: None,
711 },
712 prefix: full_prefix,
713 span: full_span,
714 }];
715 }
716 FlatShape::Block | FlatShape::Closure => {
717 let contexts = handle_block_or_closure(
718 input,
719 shapes,
720 working_set,
721 engine_guard,
722 &prefix,
723 span,
724 shape.as_str().trim_start_matches("shape_"),
725 idx,
726 local_span,
727 global_offset,
728 );
729 if !contexts.is_empty() {
730 return contexts;
731 }
732 }
733 FlatShape::Variable(var_id) => {
734 // Variable or cell path completion context
735 let trimmed_prefix = prefix.trim();
736 if trimmed_prefix.starts_with('$') {
737 // Check if this is a cell path (contains a dot after $)
738 if let Some((_, path_so_far, cell_prefix)) =
739 parse_cell_path(trimmed_prefix)
740 {
741 let prefix_byte_len = cell_prefix.len();
742 let cell_span_start = span.end.saturating_sub(prefix_byte_len);
743 return vec![CompletionContext {
744 kind: CompletionKind::CellPath {
745 var_id: *var_id,
746 path_so_far: path_so_far
747 .iter()
748 .map(|s| s.to_string())
749 .collect(),
750 },
751 prefix: cell_prefix.to_string(),
752 span: Span::new(cell_span_start, span.end),
753 }];
754 } else {
755 // Simple variable completion
756 let var_prefix = trimmed_prefix[1..].to_string();
757 return vec![CompletionContext {
758 kind: CompletionKind::Variable,
759 prefix: var_prefix,
760 span,
761 }];
762 }
763 } else {
764 // Fallback to argument context if no $ found
765 return vec![CompletionContext {
766 kind: CompletionKind::Argument,
767 prefix: prefix.to_string(),
768 span,
769 }];
770 }
771 }
772 _ => {
773 // Check if this is a variable or cell path (starts with $)
774 let trimmed_prefix = prefix.trim();
775 if trimmed_prefix.starts_with('$') {
776 // Check if this is a cell path (contains a dot after $)
777 if let Some((var_name, path_so_far, cell_prefix)) =
778 parse_cell_path(trimmed_prefix)
779 {
780 let var_id = lookup_variable_id(var_name, working_set);
781 if let Some(var_id) = var_id {
782 let prefix_byte_len = cell_prefix.len();
783 let cell_span_start = span.end.saturating_sub(prefix_byte_len);
784 return vec![CompletionContext {
785 kind: CompletionKind::CellPath {
786 var_id,
787 path_so_far: path_so_far
788 .iter()
789 .map(|s| s.to_string())
790 .collect(),
791 },
792 prefix: cell_prefix.to_string(),
793 span: Span::new(cell_span_start, span.end),
794 }];
795 } else {
796 let var_prefix = trimmed_prefix[1..].to_string();
797 return vec![CompletionContext {
798 kind: CompletionKind::Variable,
799 prefix: var_prefix,
800 span,
801 }];
802 }
803 } else {
804 // Simple variable completion
805 let var_prefix = if trimmed_prefix.len() > 1 {
806 trimmed_prefix[1..].to_string()
807 } else {
808 String::new()
809 };
810 return vec![CompletionContext {
811 kind: CompletionKind::Variable,
812 prefix: var_prefix,
813 span,
814 }];
815 }
816 } else {
817 // Use helper to determine flag or argument context
818 return determine_flag_or_argument_context(
819 input,
820 shapes,
821 &trimmed_prefix,
822 idx,
823 local_span,
824 span,
825 global_offset,
826 working_set,
827 engine_guard,
828 );
829 }
830 }
831 }
832 }
833 break;
834 }
835 }
836 Vec::new()
837}
838
839pub fn determine_context_fallback(
840 input: &str,
841 shapes: &[(Span, FlatShape)],
842 working_set: &StateWorkingSet,
843 engine_guard: &EngineState,
844 byte_pos: usize,
845 global_offset: usize,
846) -> Vec<CompletionContext> {
847 use nu_parser::{TokenContents, lex};
848
849 console_log!("[completion] Context is None, entering fallback logic");
850 // Check if there's a command-like shape before us
851 let mut has_separator_after_command = false;
852 for (span, shape) in shapes.iter().rev() {
853 let local_span = to_local_span(*span, global_offset);
854 if local_span.end <= byte_pos {
855 if is_command_shape(input, shape, local_span) {
856 // Check if there's a pipe or semicolon between this command and the cursor
857 has_separator_after_command =
858 has_separator_between(input, local_span.end, byte_pos);
859 console_log!(
860 "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
861 );
862 if !has_separator_after_command {
863 // Extract the command text (full command including subcommands)
864 let cmd = safe_slice(input, local_span);
865 let cmd_full = cmd.trim().to_string();
866 let cmd_first_word = extract_command_name(cmd).to_string();
867
868 // Check if we're right after the command (only whitespace between command and cursor)
869 let text_after_command = if local_span.end < input.len() {
870 &input[local_span.end..byte_pos]
871 } else {
872 ""
873 };
874 let is_right_after_command = text_after_command.trim().is_empty();
875
876 // If we're right after a command, check if it has positional arguments
877 if is_right_after_command {
878 // Check if the command text contains spaces (indicating it's a subcommand like "attr category")
879 let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word;
880
881 // First, try the full command name (e.g., "attr category")
882 // If that doesn't exist, fall back to the first word (e.g., "attr")
883 let full_cmd_exists =
884 get_command_signature(engine_guard, &cmd_full).is_some();
885 let cmd_name = if full_cmd_exists {
886 cmd_full.clone()
887 } else {
888 cmd_first_word.clone()
889 };
890
891 let mut context = Vec::with_capacity(2);
892 if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
893 // Check if command has any positional arguments
894 let has_positional_args = !signature.required_positional.is_empty()
895 || !signature.optional_positional.is_empty();
896
897 if has_positional_args {
898 // Count existing arguments before cursor
899 let mut arg_count = 0;
900 for (prev_span, prev_shape) in shapes.iter().rev() {
901 let prev_local_span = to_local_span(*prev_span, global_offset);
902 if prev_local_span.end <= byte_pos
903 && prev_local_span.end > local_span.end
904 {
905 if !is_command_shape(input, prev_shape, prev_local_span) {
906 let arg_text = safe_slice(input, prev_local_span);
907 let trimmed_arg = arg_text.trim();
908 // Don't count flags (starting with -) or empty arguments
909 if !trimmed_arg.is_empty()
910 && !trimmed_arg.starts_with('-')
911 {
912 arg_count += 1;
913 }
914 }
915 }
916 }
917
918 console_log!(
919 "[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}"
920 );
921
922 // Use helper to create CommandArgument context(s) - may include subcommand context
923 let arg_contexts = create_command_argument_contexts(
924 cmd_name.clone(),
925 arg_count,
926 String::new(),
927 Span::new(byte_pos, byte_pos),
928 working_set,
929 engine_guard,
930 );
931 context.extend(arg_contexts);
932 }
933 }
934 // No positional arguments
935 // If this is a subcommand (contains spaces), don't show subcommands
936 // Only show subcommands if we're using just the base command (single word)
937 if is_subcommand && full_cmd_exists {
938 console_log!(
939 "[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions"
940 );
941 } else {
942 // Show subcommands of the base command
943 console_log!(
944 "[completion] Command {cmd_name:?} has no positional args, showing subcommands"
945 );
946 context.push(CompletionContext {
947 kind: CompletionKind::Command {
948 parent_command: Some(cmd_first_word),
949 },
950 prefix: String::new(),
951 span: Span::new(byte_pos, byte_pos),
952 });
953 }
954 // reverse to put subcommands in the beginning
955 context.reverse();
956 return context;
957 } else {
958 // Not right after command, complete the command itself
959 console_log!("[completion] Set Command context with prefix: {cmd:?}");
960 return vec![CompletionContext {
961 kind: CompletionKind::Command {
962 parent_command: None,
963 },
964 prefix: cmd.to_string(),
965 span: local_span,
966 }];
967 }
968 }
969 }
970 break;
971 }
972 }
973
974 // No command found before, check context from tokens
975 console_log!("[completion] No command found before cursor, checking tokens");
976 // No command before, check context from tokens
977 let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
978 let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
979
980 let is_cmd_context = if let Some(token) = last_token {
981 let matches = matches!(
982 token.contents,
983 TokenContents::Pipe
984 | TokenContents::PipePipe
985 | TokenContents::Semicolon
986 | TokenContents::Eol
987 );
988 console_log!(
989 "[completion] Last token: {contents:?}, is_cmd_context from token={matches}",
990 contents = token.contents
991 );
992 matches
993 } else {
994 console_log!(
995 "[completion] No last token found, assuming start of input (is_cmd_context=true)"
996 );
997 true // Start of input
998 };
999
1000 // Look for the last non-whitespace token before cursor
1001 let text_before = &input[..byte_pos];
1002
1003 // Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
1004 let text_before_trimmed = text_before.trim_end();
1005 let is_inside_block = text_before_trimmed.ends_with('{');
1006 // If we found a separator after a command, we're starting a new command
1007 let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
1008 console_log!(
1009 "[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}"
1010 );
1011
1012 // Find the last word before cursor
1013 let last_word_start = text_before
1014 .rfind(|c: char| c.is_whitespace() || is_separator_char(c))
1015 .map(|i| i + 1)
1016 .unwrap_or(0);
1017
1018 let last_word = text_before[last_word_start..].trim_start();
1019 console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}");
1020
1021 if is_cmd_context {
1022 vec![CompletionContext {
1023 kind: CompletionKind::Command {
1024 parent_command: None,
1025 },
1026 prefix: last_word.to_string(),
1027 span: Span::new(last_word_start, byte_pos),
1028 }]
1029 } else {
1030 // Check if this is a variable or cell path (starts with $)
1031 let trimmed_word = last_word.trim();
1032 if trimmed_word.starts_with('$') {
1033 // Check if this is a cell path (contains a dot after $)
1034 if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) {
1035 let var_id = lookup_variable_id(&var_name, working_set);
1036
1037 if let Some(var_id) = var_id {
1038 let prefix_byte_len = cell_prefix.len();
1039 let cell_span_start = byte_pos.saturating_sub(prefix_byte_len);
1040 vec![CompletionContext {
1041 kind: CompletionKind::CellPath {
1042 var_id,
1043 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
1044 },
1045 prefix: cell_prefix.to_string(),
1046 span: Span::new(cell_span_start, byte_pos),
1047 }]
1048 } else {
1049 let var_prefix = trimmed_word[1..].to_string();
1050 vec![CompletionContext {
1051 kind: CompletionKind::Variable,
1052 prefix: var_prefix,
1053 span: Span::new(last_word_start, byte_pos),
1054 }]
1055 }
1056 } else {
1057 // Simple variable completion
1058 let var_prefix = trimmed_word[1..].to_string();
1059 vec![CompletionContext {
1060 kind: CompletionKind::Variable,
1061 prefix: var_prefix,
1062 span: Span::new(last_word_start, byte_pos),
1063 }]
1064 }
1065 } else if trimmed_word.starts_with('-') {
1066 // Try to find command by looking backwards through shapes
1067 let mut found_cmd = None;
1068 for (span, shape) in shapes.iter().rev() {
1069 let local_span = to_local_span(*span, global_offset);
1070 if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) {
1071 let cmd_text = safe_slice(input, local_span);
1072 let cmd_name = extract_command_name(cmd_text).to_string();
1073 found_cmd = Some(cmd_name);
1074 break;
1075 }
1076 }
1077 if let Some(cmd_name) = found_cmd {
1078 vec![CompletionContext {
1079 kind: CompletionKind::Flag {
1080 command_name: cmd_name,
1081 },
1082 prefix: trimmed_word.to_string(),
1083 span: Span::new(last_word_start, byte_pos),
1084 }]
1085 } else {
1086 vec![CompletionContext {
1087 kind: CompletionKind::Argument,
1088 prefix: last_word.to_string(),
1089 span: Span::new(last_word_start, byte_pos),
1090 }]
1091 }
1092 } else {
1093 // Try to find command and argument index
1094 let mut found_cmd = None;
1095 let mut arg_count = 0;
1096 for (span, shape) in shapes.iter().rev() {
1097 let local_span = to_local_span(*span, global_offset);
1098 if local_span.end <= byte_pos {
1099 if is_command_shape(input, shape, local_span) {
1100 let cmd_text = safe_slice(input, local_span);
1101 let cmd_name = extract_command_name(cmd_text).to_string();
1102 found_cmd = Some(cmd_name);
1103 break;
1104 } else {
1105 let arg_text = safe_slice(input, local_span);
1106 let trimmed_arg = arg_text.trim();
1107 if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
1108 arg_count += 1;
1109 }
1110 }
1111 }
1112 }
1113 if let Some(cmd_name) = found_cmd {
1114 create_command_argument_contexts(
1115 cmd_name,
1116 arg_count,
1117 trimmed_word.to_string(),
1118 Span::new(last_word_start, byte_pos),
1119 working_set,
1120 engine_guard,
1121 )
1122 } else {
1123 vec![CompletionContext {
1124 kind: CompletionKind::Argument,
1125 prefix: last_word.to_string(),
1126 span: Span::new(last_word_start, byte_pos),
1127 }]
1128 }
1129 }
1130 }
1131}
1132
1133pub fn determine_context(
1134 input: &str,
1135 shapes: &[(Span, FlatShape)],
1136 working_set: &StateWorkingSet,
1137 engine_guard: &EngineState,
1138 byte_pos: usize,
1139 global_offset: usize,
1140) -> Vec<CompletionContext> {
1141 // First try to determine context from shapes
1142 let contexts = determine_context_from_shape(
1143 input,
1144 shapes,
1145 working_set,
1146 engine_guard,
1147 byte_pos,
1148 global_offset,
1149 );
1150 if !contexts.is_empty() {
1151 return contexts;
1152 }
1153
1154 // Fallback to token-based context determination
1155 determine_context_fallback(
1156 input,
1157 shapes,
1158 working_set,
1159 engine_guard,
1160 byte_pos,
1161 global_offset,
1162 )
1163}