use super::completer::touches;
use crate::{
CompletionEngine, FileCompletion,
completions::{
Completer, Context, DirectoryCompletion, ExportableCompletion, Fetched, SemanticSuggestion,
completion_options::NuMatcher, to_reedline_span,
},
};
use nu_parser::parse_module_file_or_dir;
use nu_protocol::{
DynamicCompletionCallRef, Span,
ast::{Argument, Call, Expr, Expression, ListItem},
engine::{ArgType, StateWorkingSet},
};
pub struct ArgValueCompletion<'a> {
pub call: &'a Call,
pub arg_type: ArgType<'a>,
pub need_fallback: bool,
pub completer: &'a CompletionEngine,
pub arg_idx: usize,
pub cursor: usize,
}
impl<'a> Completer for ArgValueCompletion<'a> {
fn fetch(&mut self, context: &Context) -> Fetched {
if let Some(fetched_completion) = self.try_fetch_dynamic_completion(context) {
return fetched_completion;
}
let working_set = context.working_set;
let prefix_string = context.prefix_str();
let completion_context = self.completer.context(
working_set,
context.span,
prefix_string.as_ref().as_bytes(),
context.offset,
);
self.fetch_fallback_completion(self.arg_expr(), &completion_context)
}
}
impl<'a> ArgValueCompletion<'a> {
fn try_fetch_dynamic_completion(&self, context: &Context) -> Option<Fetched> {
let working_set = context.working_set;
let declaration = working_set.get_decl(self.call.decl_id);
let mut stack = context.stack.to_owned();
let dynamic_completion_call = DynamicCompletionCallRef {
call: self.call,
strip: false,
pos: self.cursor,
};
let completion_result = declaration.get_dynamic_completion(
working_set.permanent_state,
&mut stack,
dynamic_completion_call,
&self.arg_type,
#[expect(deprecated, reason = "internal usage")]
nu_protocol::engine::ExperimentalMarker,
);
match completion_result {
Ok(Some(items)) => {
let prefix = context.prefix_str();
let mut matcher = NuMatcher::new(prefix.as_ref(), context.options, true);
for suggestion_item in items {
let result_span = suggestion_item
.span
.filter(|span| span.start >= context.offset && span.end >= span.start)
.unwrap_or(context.span);
let suggestion = SemanticSuggestion::from_dynamic_suggestion(
suggestion_item,
to_reedline_span(result_span, context.offset),
None,
);
matcher.add_semantic_suggestion(suggestion);
}
Some(Fetched::cacheable(matcher.suggestion_results()))
}
Ok(None) => None, Err(error) => {
log::error!(
"error on fetching dynamic suggestion on {} with {:?}: {error}",
declaration.name(),
self.arg_type
);
None
}
}
}
fn arg_expr(&self) -> Option<&Expr> {
self.call
.arguments
.get(self.arg_idx)
.and_then(|argument| argument.expr())
.map(|expression_wrapper| &expression_wrapper.expr)
}
pub(crate) fn complete_module_exports(
&self,
completion_context: &Context,
working_set: &StateWorkingSet,
) -> Fetched {
let expression = self.arg_expr();
let Some((module_name, span)) = self.find_module_name_and_span() else {
return Fetched::pure(vec![]);
};
let Some((module_id, temp_working_set)) =
self.resolve_module(working_set, module_name, span)
else {
return Fetched::pure(vec![]);
};
let mut exportable_completion = ExportableCompletion {
module_id,
temp_working_set,
};
let fetched_result = match expression {
Some(Expr::FullCellPath(full_cell_path)) => match &full_cell_path.head.expr {
Expr::List(items) => self.complete_on_list_items(
items,
completion_context,
&mut exportable_completion,
),
_ => Fetched::pure(vec![]),
},
_ => exportable_completion.fetch(completion_context),
};
fetched_result.caching()
}
fn find_module_name_and_span(&self) -> Option<(&[u8], Span)> {
let first = self.call.arguments.iter().find_map(|a| {
if let Argument::Positional(e) = a {
Some(e)
} else {
None
}
})?;
if let Expression {
expr: Expr::String(name),
span,
..
} = first
{
Some((name.as_bytes(), *span))
} else {
None
}
}
fn resolve_module<'set>(
&self,
working_set: &'set StateWorkingSet,
module_name: &[u8],
span: Span,
) -> Option<(nu_protocol::ModuleId, Option<StateWorkingSet<'set>>)> {
if let Some(module_id) = working_set.find_module(module_name) {
return Some((module_id, None));
}
let mut temp_working_set = StateWorkingSet::new(working_set.permanent_state);
let module_id = parse_module_file_or_dir(&mut temp_working_set, module_name, span, None)?;
Some((module_id, Some(temp_working_set)))
}
fn complete_on_list_items(
&self,
items: &[ListItem],
completion_context: &Context,
exportable_completion: &mut ExportableCompletion,
) -> Fetched {
let cursor_position = self.cursor;
let item_span = items
.iter()
.map(|item| item.expr().span)
.find(|item_span| touches(*item_span, cursor_position))
.unwrap_or(Span::point(cursor_position));
let relative_offset = item_span
.start
.saturating_sub(completion_context.span.start);
let sliced_prefix = completion_context
.prefix
.get(relative_offset..)
.unwrap_or_default();
let new_span = Span::new(
item_span.start,
completion_context.span.end.min(item_span.end),
);
let item_context = self.completer.context(
completion_context.working_set,
new_span,
sliced_prefix,
completion_context.offset,
);
exportable_completion.fetch(&item_context)
}
fn fetch_fallback_completion(
&self,
expression: Option<&Expr>,
completion_context: &Context,
) -> Fetched {
let complete_file = || FileCompletion.fetch(completion_context);
match expression {
Some(Expr::Directory(_, _)) => DirectoryCompletion.fetch(completion_context),
Some(Expr::Filepath(_, _)) | Some(Expr::GlobPattern(_, _)) => complete_file(),
_ if self.need_fallback => complete_file(),
_ => Fetched::pure(vec![]),
}
}
}