use crate::model::{
MayaAddAttrCommand, MayaAddAttrTailKind, MayaConnectAttrCommand, MayaCreateNodeCommand,
MayaCurrentUnitCommand, MayaFileCommand, MayaFileInfoCommand, MayaLightAddAttrCommand,
MayaLightConnectAttrCommand, MayaLightCreateNodeCommand, MayaLightCurrentUnitCommand,
MayaLightFileCommand, MayaLightFileInfoCommand, MayaLightFlag, MayaLightRelationshipCommand,
MayaLightRenameCommand, MayaLightRequiresCommand, MayaLightSelectCommand,
MayaLightSetAttrCommand, MayaLightSpecializedCommand, MayaNormalizedCommand,
MayaNormalizedFlag, MayaRawShellItem, MayaRawShellItemKind, MayaRelationshipCommand,
MayaRenameCommand, MayaRequiresCommand, MayaSelectCommand, MayaSetAttrCommand,
MayaSetAttrValueKind, MayaSpecializedCommand,
};
use crate::normalize::{LightParseLike, normalized_flags, normalized_positionals};
use mel_sema::{CommandSchema, FlagArity, FlagSchema};
use mel_syntax::{SourceView, TextRange, range_end, range_start, text_range};
pub(crate) fn specialize_command(
source: SourceView<'_>,
head: &str,
span: TextRange,
normalized: Option<&MayaNormalizedCommand>,
raw_items: &[MayaRawShellItem],
) -> Option<MayaSpecializedCommand> {
let normalized = normalized?;
let flags = normalized_flags(normalized);
let positionals = normalized_positionals(normalized);
match normalized.schema_name.as_str() {
"requires" => Some(MayaSpecializedCommand::Requires(MayaRequiresCommand {
plugin_name: positionals.first().cloned(),
plugin_version: positionals.get(1).cloned(),
option_items: flatten_flag_items(&flags),
requirements: positionals,
flags,
span,
})),
"currentUnit" => Some(MayaSpecializedCommand::CurrentUnit(
MayaCurrentUnitCommand {
linear: first_flag_arg(&flags, "linear"),
angle: first_flag_arg(&flags, "angle"),
time: first_flag_arg(&flags, "time"),
flags,
span,
},
)),
"fileInfo" => Some(MayaSpecializedCommand::FileInfo(MayaFileInfoCommand {
key: positionals.first().cloned(),
value: positionals.get(1).cloned(),
flags,
span,
})),
"createNode" => Some(MayaSpecializedCommand::CreateNode(MayaCreateNodeCommand {
node_type: positionals.first().cloned(),
name: first_flag_arg(&flags, "name"),
parent: first_flag_arg(&flags, "parent"),
shared: has_flag(&flags, "shared"),
flags,
span,
})),
"rename" => {
let has_uuid = has_flag(&flags, "uuid");
Some(MayaSpecializedCommand::Rename(MayaRenameCommand {
uuid: if has_uuid {
positionals.first().cloned()
} else {
None
},
source: positionals.get(usize::from(has_uuid)).cloned(),
target: positionals.get(usize::from(has_uuid) + 1).cloned(),
flags,
span,
}))
}
"select" => Some(MayaSpecializedCommand::Select(MayaSelectCommand {
no_expand: has_flag(&flags, "noExpand"),
targets: positionals,
flags,
span,
})),
"setAttr" => Some(MayaSpecializedCommand::SetAttr(specialize_set_attr(
source,
span,
&flags,
&positionals,
))),
"addAttr" => Some(MayaSpecializedCommand::AddAttr(Box::new(
specialize_add_attr(span, &flags, &positionals),
))),
"connectAttr" => Some(MayaSpecializedCommand::ConnectAttr(
MayaConnectAttrCommand {
source_attr: positionals.first().cloned(),
target_attr: positionals.get(1).cloned(),
next_available: has_flag(&flags, "nextAvailable"),
lock_arg: first_flag_arg(&flags, "lock"),
flags,
span,
},
)),
"relationship" => Some(MayaSpecializedCommand::Relationship(
MayaRelationshipCommand {
relationship: positionals.first().cloned(),
members: positionals.into_iter().skip(1).collect(),
flags,
span,
},
)),
"file" => Some(MayaSpecializedCommand::File(MayaFileCommand {
path: positionals
.last()
.cloned()
.or_else(|| raw_items.last().cloned()),
namespace: first_flag_arg(&flags, "namespace"),
reference_node: first_flag_arg(&flags, "referenceNode"),
file_type: first_flag_arg(&flags, "type"),
options: first_flag_arg(&flags, "options"),
is_reference: has_flag(&flags, "reference") || has_flag(&flags, "referenceDepthInfo"),
flags,
span,
})),
_ => {
let _ = source;
let _ = head;
None
}
}
}
pub(crate) fn specialize_light_command(
parse: &impl LightParseLike,
head: &str,
span: TextRange,
opaque_tail: Option<TextRange>,
schema: &CommandSchema,
prefix_items: &[MayaRawShellItem],
) -> Option<MayaLightSpecializedCommand> {
let (flags, positionals) = normalize_light_items(parse, schema, prefix_items);
match schema.name.as_ref() {
"requires" => Some(MayaLightSpecializedCommand::Requires(
MayaLightRequiresCommand {
plugin_name: positionals.first().cloned(),
plugin_version: positionals.get(1).cloned(),
option_items: flatten_light_flag_items(&flags),
requirements: positionals,
flags,
opaque_tail,
span,
},
)),
"currentUnit" => Some(MayaLightSpecializedCommand::CurrentUnit(
MayaLightCurrentUnitCommand {
linear: first_light_flag_arg(&flags, "linear"),
angle: first_light_flag_arg(&flags, "angle"),
time: first_light_flag_arg(&flags, "time"),
flags,
opaque_tail,
span,
},
)),
"fileInfo" => Some(MayaLightSpecializedCommand::FileInfo(
MayaLightFileInfoCommand {
key: positionals.first().cloned(),
value: positionals.get(1).cloned(),
flags,
opaque_tail,
span,
},
)),
"createNode" => Some(MayaLightSpecializedCommand::CreateNode(
MayaLightCreateNodeCommand {
node_type: positionals.first().cloned(),
name: first_light_flag_arg(&flags, "name"),
parent: first_light_flag_arg(&flags, "parent"),
shared: has_light_flag(&flags, "shared"),
flags,
opaque_tail,
span,
},
)),
"rename" => {
let has_uuid = has_light_flag(&flags, "uuid");
Some(MayaLightSpecializedCommand::Rename(
MayaLightRenameCommand {
uuid: if has_uuid {
positionals.first().cloned()
} else {
None
},
source: positionals.get(usize::from(has_uuid)).cloned(),
target: positionals.get(usize::from(has_uuid) + 1).cloned(),
flags,
opaque_tail,
span,
},
))
}
"select" => Some(MayaLightSpecializedCommand::Select(
MayaLightSelectCommand {
no_expand: has_light_flag(&flags, "noExpand"),
targets: positionals,
flags,
opaque_tail,
span,
},
)),
"setAttr" => Some(MayaLightSpecializedCommand::SetAttr(
specialize_light_set_attr(parse, span, opaque_tail, &flags, &positionals),
)),
"addAttr" => Some(MayaLightSpecializedCommand::AddAttr(Box::new(
specialize_light_add_attr(span, opaque_tail, &flags, &positionals),
))),
"connectAttr" => Some(MayaLightSpecializedCommand::ConnectAttr(
MayaLightConnectAttrCommand {
source_attr: positionals.first().cloned(),
target_attr: positionals.get(1).cloned(),
next_available: has_light_flag(&flags, "nextAvailable"),
lock_arg: first_light_flag_arg(&flags, "lock"),
flags,
opaque_tail,
span,
},
)),
"relationship" => Some(MayaLightSpecializedCommand::Relationship(
MayaLightRelationshipCommand {
relationship: positionals.first().cloned(),
members: positionals.into_iter().skip(1).collect(),
flags,
opaque_tail,
span,
},
)),
"file" => Some(MayaLightSpecializedCommand::File(MayaLightFileCommand {
path: positionals.last().cloned(),
namespace: first_light_flag_arg(&flags, "namespace"),
reference_node: first_light_flag_arg(&flags, "referenceNode"),
file_type: first_light_flag_arg(&flags, "type"),
options: first_light_flag_arg(&flags, "options"),
is_reference: has_light_flag(&flags, "reference")
|| has_light_flag(&flags, "referenceDepthInfo"),
flags,
opaque_tail,
span,
})),
_ => {
let _ = parse;
let _ = head;
None
}
}
}
fn specialize_set_attr(
source: SourceView<'_>,
span: TextRange,
flags: &[MayaNormalizedFlag],
positionals: &[MayaRawShellItem],
) -> MayaSetAttrCommand {
let attr_path = positionals.first().cloned();
let values = positionals.iter().skip(1).cloned().collect::<Vec<_>>();
let type_name = first_flag_arg(flags, "type");
let type_text = type_name
.as_ref()
.and_then(|item| item.value_text(source))
.unwrap_or_default();
let value_kind = match type_text {
"string" if values.len() == 1 => MayaSetAttrValueKind::String,
"stringArray" => MayaSetAttrValueKind::StringArray,
"Int32Array" => MayaSetAttrValueKind::Int32Array,
"componentList" => MayaSetAttrValueKind::ComponentList,
"matrix" | "matrixXform" => MayaSetAttrValueKind::MatrixXform,
"dataReferenceEdits" => MayaSetAttrValueKind::DataReferenceEdits,
"" if values.len() == 1 && matches!(values[0].kind, MayaRawShellItemKind::QuotedString) => {
MayaSetAttrValueKind::String
}
_ if values.iter().all(is_numeric_like) => MayaSetAttrValueKind::TypedNumbers,
_ if !type_text.is_empty() => MayaSetAttrValueKind::OpaqueTyped,
_ => MayaSetAttrValueKind::Unknown,
};
MayaSetAttrCommand {
attr_path,
type_name,
value_kind,
values,
flags: flags.to_vec(),
span,
}
}
fn specialize_light_set_attr(
parse: &impl LightParseLike,
span: TextRange,
opaque_tail: Option<TextRange>,
flags: &[MayaLightFlag],
positionals: &[MayaRawShellItem],
) -> MayaLightSetAttrCommand {
let attr_path = positionals.first().cloned();
let prefix_values = positionals.iter().skip(1).cloned().collect::<Vec<_>>();
let type_name = first_light_flag_arg(flags, "type");
let type_text = type_name
.as_ref()
.and_then(|item| item.value_text(parse.source_view()))
.unwrap_or_default();
let value_kind = match type_text {
"string" if prefix_values.len() == 1 && opaque_tail.is_none() => {
MayaSetAttrValueKind::String
}
"stringArray" => MayaSetAttrValueKind::StringArray,
"Int32Array" => MayaSetAttrValueKind::Int32Array,
"componentList" => MayaSetAttrValueKind::ComponentList,
"matrix" | "matrixXform" => MayaSetAttrValueKind::MatrixXform,
"dataReferenceEdits" => MayaSetAttrValueKind::DataReferenceEdits,
"" if prefix_values.len() == 1
&& opaque_tail.is_none()
&& matches!(prefix_values[0].kind, MayaRawShellItemKind::QuotedString) =>
{
MayaSetAttrValueKind::String
}
_ if opaque_tail.is_none() && prefix_values.iter().all(is_numeric_like) => {
MayaSetAttrValueKind::TypedNumbers
}
_ if !type_text.is_empty() => MayaSetAttrValueKind::OpaqueTyped,
_ => MayaSetAttrValueKind::Unknown,
};
MayaLightSetAttrCommand {
attr_path,
type_name,
value_kind,
prefix_values,
flags: flags.to_vec(),
opaque_tail,
span,
}
}
fn specialize_add_attr(
span: TextRange,
flags: &[MayaNormalizedFlag],
positionals: &[MayaRawShellItem],
) -> MayaAddAttrCommand {
MayaAddAttrCommand {
short_name: first_flag_arg(flags, "shortName"),
long_name: first_flag_arg(flags, "longName"),
parent: first_flag_arg(flags, "parent"),
number_of_children: first_flag_arg(flags, "numberOfChildren"),
nice_name: first_flag_arg(flags, "niceName"),
default_value: first_flag_arg(flags, "defaultValue"),
min_value: first_flag_arg(flags, "minValue"),
max_value: first_flag_arg(flags, "maxValue"),
soft_min_value: first_flag_arg(flags, "softMinValue"),
soft_max_value: first_flag_arg(flags, "softMaxValue"),
enum_name: first_flag_arg(flags, "enumName"),
attribute_type: first_flag_arg(flags, "attributeType"),
data_type: first_flag_arg(flags, "dataType"),
flags: flags.to_vec(),
tail: positionals.to_vec(),
tail_kind: classify_add_attr_tail(positionals),
span,
}
}
fn specialize_light_add_attr(
span: TextRange,
opaque_tail: Option<TextRange>,
flags: &[MayaLightFlag],
positionals: &[MayaRawShellItem],
) -> MayaLightAddAttrCommand {
MayaLightAddAttrCommand {
short_name: first_light_flag_arg(flags, "shortName"),
long_name: first_light_flag_arg(flags, "longName"),
parent: first_light_flag_arg(flags, "parent"),
number_of_children: first_light_flag_arg(flags, "numberOfChildren"),
nice_name: first_light_flag_arg(flags, "niceName"),
default_value: first_light_flag_arg(flags, "defaultValue"),
min_value: first_light_flag_arg(flags, "minValue"),
max_value: first_light_flag_arg(flags, "maxValue"),
soft_min_value: first_light_flag_arg(flags, "softMinValue"),
soft_max_value: first_light_flag_arg(flags, "softMaxValue"),
enum_name: first_light_flag_arg(flags, "enumName"),
attribute_type: first_light_flag_arg(flags, "attributeType"),
data_type: first_light_flag_arg(flags, "dataType"),
flags: flags.to_vec(),
tail: positionals.to_vec(),
tail_kind: classify_add_attr_tail(positionals),
opaque_tail,
span,
}
}
pub(crate) fn classify_add_attr_tail(positionals: &[MayaRawShellItem]) -> MayaAddAttrTailKind {
if positionals.is_empty() {
return MayaAddAttrTailKind::None;
}
if positionals.iter().all(is_numeric_like) {
return MayaAddAttrTailKind::Numeric;
}
if positionals
.iter()
.all(|item| matches!(item.kind, MayaRawShellItemKind::QuotedString))
{
return MayaAddAttrTailKind::String;
}
MayaAddAttrTailKind::Mixed
}
pub(crate) fn first_flag_arg(
flags: &[MayaNormalizedFlag],
canonical_name: &str,
) -> Option<MayaRawShellItem> {
flags
.iter()
.find(|flag| flag.canonical_name.as_deref() == Some(canonical_name))
.and_then(|flag| flag.args.first())
.map(|arg| arg.item.clone())
}
pub(crate) fn has_flag(flags: &[MayaNormalizedFlag], canonical_name: &str) -> bool {
flags
.iter()
.any(|flag| flag.canonical_name.as_deref() == Some(canonical_name))
}
pub(crate) fn first_light_flag_arg(
flags: &[MayaLightFlag],
canonical_name: &str,
) -> Option<MayaRawShellItem> {
flags
.iter()
.find(|flag| flag.canonical_name.as_deref() == Some(canonical_name))
.and_then(|flag| flag.args.first())
.map(|arg| arg.item.clone())
}
pub(crate) fn has_light_flag(flags: &[MayaLightFlag], canonical_name: &str) -> bool {
flags
.iter()
.any(|flag| flag.canonical_name.as_deref() == Some(canonical_name))
}
fn flatten_flag_items(flags: &[MayaNormalizedFlag]) -> Vec<MayaRawShellItem> {
let mut items = Vec::new();
for flag in flags {
items.push(flag_item(flag.source_range));
items.extend(flag.args.iter().map(|arg| arg.item.clone()));
}
items
}
fn flatten_light_flag_items(flags: &[MayaLightFlag]) -> Vec<MayaRawShellItem> {
let mut items = Vec::new();
for flag in flags {
items.push(flag_item(flag.source_range));
items.extend(flag.args.iter().map(|arg| arg.item.clone()));
}
items
}
fn flag_item(source_range: TextRange) -> MayaRawShellItem {
MayaRawShellItem {
kind: MayaRawShellItemKind::Flag,
span: source_range,
text_range: Some(source_range),
}
}
pub(crate) fn is_numeric_like(item: &MayaRawShellItem) -> bool {
matches!(item.kind, MayaRawShellItemKind::Numeric)
}
fn normalize_light_items(
parse: &impl LightParseLike,
schema: &CommandSchema,
items: &[MayaRawShellItem],
) -> (Vec<MayaLightFlag>, Vec<MayaRawShellItem>) {
let mode = detect_light_mode(parse, schema, items);
let mut index = 0;
let mut flags = Vec::new();
let mut positionals = Vec::new();
while index < items.len() {
let item = &items[index];
if item.kind != MayaRawShellItemKind::Flag {
positionals.push(item.clone());
index += 1;
continue;
}
let schema_flag = find_flag_schema(schema, item.source_text(parse.source_view()));
let expected_arity = schema_flag
.as_ref()
.map(|flag| arity_for_mode(flag.arity_by_mode, mode))
.unwrap_or(FlagArity::None);
let (_, max_arity) = arity_bounds(expected_arity);
let mut args = Vec::new();
let mut consumed = 0;
while consumed < max_arity {
let Some(next_item) = items.get(index + 1 + consumed) else {
break;
};
if next_item.kind == MayaRawShellItemKind::Flag {
break;
}
args.push(crate::model::MayaPositionalArg {
item: next_item.clone(),
});
consumed += 1;
}
let span = args.last().map_or(item.span, |arg| {
text_range(range_start(item.span), range_end(arg.item.span))
});
flags.push(MayaLightFlag {
source_range: item.span,
canonical_name: schema_flag.as_ref().map(|flag| flag.long_name.to_string()),
args,
span,
});
index += 1 + consumed;
}
(flags, positionals)
}
fn detect_light_mode(
parse: &impl LightParseLike,
schema: &CommandSchema,
items: &[MayaRawShellItem],
) -> mel_sema::CommandMode {
let mut create = false;
let mut edit = false;
let mut query = false;
for item in items {
if item.kind != MayaRawShellItemKind::Flag {
continue;
}
match item
.source_text(parse.source_view())
.trim_start_matches('-')
{
"create" | "c" if schema.mode_mask.create => create = true,
"edit" | "e" if schema.mode_mask.edit => edit = true,
"query" | "q" if schema.mode_mask.query => query = true,
_ => {}
}
}
match (create, edit, query) {
(false, false, false) | (true, false, false) => mel_sema::CommandMode::Create,
(false, true, false) => mel_sema::CommandMode::Edit,
(false, false, true) => mel_sema::CommandMode::Query,
_ => mel_sema::CommandMode::Unknown,
}
}
fn find_flag_schema(command: &CommandSchema, text: &str) -> Option<FlagSchema> {
let normalized = text.strip_prefix('-').unwrap_or(text);
command
.flags
.iter()
.find(|flag| {
normalized == flag.long_name.as_ref()
|| flag
.short_name
.as_deref()
.is_some_and(|short| short == normalized)
})
.cloned()
.or_else(|| synthetic_mode_flag_for_name(command, normalized))
}
fn synthetic_mode_flag_for_name(command: &CommandSchema, name: &str) -> Option<FlagSchema> {
match name {
"create" | "c" if command.mode_mask.create => Some(synthetic_mode_flag("create", "c")),
"edit" | "e" if command.mode_mask.edit => Some(synthetic_mode_flag("edit", "e")),
"query" | "q" if command.mode_mask.query => Some(synthetic_mode_flag("query", "q")),
_ => None,
}
}
fn synthetic_mode_flag(long_name: &str, short_name: &str) -> FlagSchema {
FlagSchema {
long_name: long_name.into(),
short_name: Some(short_name.into()),
mode_mask: mel_sema::CommandModeMask {
create: true,
edit: true,
query: true,
},
arity_by_mode: mel_sema::FlagArityByMode {
create: FlagArity::None,
edit: FlagArity::None,
query: FlagArity::None,
},
value_shapes: Vec::new().into(),
allows_multiple: false,
}
}
fn arity_for_mode(
arity_by_mode: mel_sema::FlagArityByMode,
mode: mel_sema::CommandMode,
) -> FlagArity {
match mode {
mel_sema::CommandMode::Create | mel_sema::CommandMode::Unknown => arity_by_mode.create,
mel_sema::CommandMode::Edit => arity_by_mode.edit,
mel_sema::CommandMode::Query => arity_by_mode.query,
}
}
fn arity_bounds(arity: FlagArity) -> (usize, usize) {
match arity {
FlagArity::None => (0, 0),
FlagArity::Exact(value) => (usize::from(value), usize::from(value)),
FlagArity::Range { min, max } => (usize::from(min), usize::from(max)),
}
}