use crate::model::{
MayaNormalizedCommand, MayaNormalizedCommandItem, MayaNormalizedFlag, MayaPositionalArg,
MayaRawShellItem, MayaRawShellItemKind, MayaTopLevelItem,
};
use mel_ast::{ProcDef, ShellWord, SourceFile, Stmt};
use mel_parser::{LightParse, LightSourceFile, LightWord, Parse, SharedLightParse, SharedParse};
use mel_sema::{
CommandMode, CommandSchema, FlagArity, FlagSchema, NormalizedCommandItem, NormalizedFlag,
PositionalArg,
};
use mel_syntax::{SourceView, TextRange, range_end, range_start, text_range};
use std::collections::HashMap;
pub(crate) trait FullParseLike {
fn syntax(&self) -> &SourceFile;
fn source_view(&self) -> SourceView<'_>;
fn source_slice(&self, range: TextRange) -> &str;
}
impl FullParseLike for Parse {
fn syntax(&self) -> &SourceFile {
&self.syntax
}
fn source_view(&self) -> SourceView<'_> {
Parse::source_view(self)
}
fn source_slice(&self, range: TextRange) -> &str {
Parse::source_slice(self, range)
}
}
impl FullParseLike for SharedParse {
fn syntax(&self) -> &SourceFile {
&self.syntax
}
fn source_view(&self) -> SourceView<'_> {
SharedParse::source_view(self)
}
fn source_slice(&self, range: TextRange) -> &str {
SharedParse::source_slice(self, range)
}
}
pub(crate) trait LightParseLike {
fn light_source(&self) -> &LightSourceFile;
fn source_view(&self) -> SourceView<'_>;
fn source_slice(&self, range: TextRange) -> &str;
}
impl LightParseLike for LightParse {
fn light_source(&self) -> &LightSourceFile {
&self.source
}
fn source_view(&self) -> SourceView<'_> {
LightParse::source_view(self)
}
fn source_slice(&self, range: TextRange) -> &str {
LightParse::source_slice(self, range)
}
}
impl LightParseLike for SharedLightParse {
fn light_source(&self) -> &LightSourceFile {
&self.source
}
fn source_view(&self) -> SourceView<'_> {
SharedLightParse::source_view(self)
}
fn source_slice(&self, range: TextRange) -> &str {
SharedLightParse::source_slice(self, range)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct NormalizedInvokeKey {
head_range: TextRange,
span: TextRange,
}
impl NormalizedInvokeKey {
const fn new(head_range: TextRange, span: TextRange) -> Self {
Self { head_range, span }
}
}
pub(crate) fn normalized_invoke_lookup_from_parse(
parse: &impl FullParseLike,
invokes: Vec<mel_sema::NormalizedCommandInvoke>,
) -> HashMap<NormalizedInvokeKey, MayaNormalizedCommand> {
normalized_invoke_lookup_from_source(parse.source_view(), invokes)
}
pub(crate) fn normalized_invoke_lookup_from_source(
source: SourceView<'_>,
invokes: Vec<mel_sema::NormalizedCommandInvoke>,
) -> HashMap<NormalizedInvokeKey, MayaNormalizedCommand> {
let mut lookup = HashMap::with_capacity(invokes.len());
for invoke in invokes {
let normalized = maya_normalized_command_from_source(source, invoke);
let key = NormalizedInvokeKey::new(normalized.head_range, normalized.span);
let previous = lookup.insert(key, normalized);
debug_assert!(
previous.is_none(),
"duplicate normalized invoke for {key:?}"
);
}
lookup
}
pub(crate) fn take_matching_normalized(
invokes: &mut HashMap<NormalizedInvokeKey, MayaNormalizedCommand>,
head_range: TextRange,
range: TextRange,
) -> Option<MayaNormalizedCommand> {
invokes.remove(&NormalizedInvokeKey::new(head_range, range))
}
pub(crate) fn stmt_range(stmt: &Stmt) -> TextRange {
match stmt {
Stmt::Empty { range }
| Stmt::Proc { range, .. }
| Stmt::Block { range, .. }
| Stmt::Expr { range, .. }
| Stmt::VarDecl { range, .. }
| Stmt::If { range, .. }
| Stmt::While { range, .. }
| Stmt::DoWhile { range, .. }
| Stmt::Switch { range, .. }
| Stmt::For { range, .. }
| Stmt::ForIn { range, .. }
| Stmt::Return { range, .. }
| Stmt::Break { range }
| Stmt::Continue { range } => *range,
}
}
pub(crate) fn proc_item(parse: &impl FullParseLike, proc_def: &ProcDef) -> MayaTopLevelItem {
MayaTopLevelItem::Proc {
name: parse.source_slice(proc_def.name_range).to_owned(),
is_global: proc_def.is_global,
span: proc_def.range,
}
}
pub(crate) fn normalized_flags(command: &MayaNormalizedCommand) -> Vec<MayaNormalizedFlag> {
command
.items
.iter()
.filter_map(|item| match item {
MayaNormalizedCommandItem::Flag(flag) => Some(flag.clone()),
MayaNormalizedCommandItem::Positional(_) => None,
})
.collect()
}
pub(crate) fn normalized_positionals(command: &MayaNormalizedCommand) -> Vec<MayaRawShellItem> {
command
.items
.iter()
.filter_map(|item| match item {
MayaNormalizedCommandItem::Flag(_) => None,
MayaNormalizedCommandItem::Positional(arg) => Some(arg.item.clone()),
})
.collect()
}
pub(crate) fn raw_item_from_shell_word(
parse: &impl FullParseLike,
word: &ShellWord,
) -> MayaRawShellItem {
raw_item_from_shell_word_with_source(parse.source_view(), word)
}
pub(crate) fn raw_item_from_shell_word_with_source(
source: SourceView<'_>,
word: &ShellWord,
) -> MayaRawShellItem {
let _ = source;
let (kind, span) = match word {
ShellWord::Flag { range, .. } => (MayaRawShellItemKind::Flag, *range),
ShellWord::NumericLiteral { range, .. } => (MayaRawShellItemKind::Numeric, *range),
ShellWord::BareWord { range, .. } => (MayaRawShellItemKind::BareWord, *range),
ShellWord::QuotedString { range, .. } => (MayaRawShellItemKind::QuotedString, *range),
ShellWord::Variable { range, .. } => (MayaRawShellItemKind::Variable, *range),
ShellWord::GroupedExpr { range, .. } => (MayaRawShellItemKind::GroupedExpr, *range),
ShellWord::BraceList { range, .. } => (MayaRawShellItemKind::BraceList, *range),
ShellWord::VectorLiteral { range, .. } => (MayaRawShellItemKind::VectorLiteral, *range),
ShellWord::Capture { range, .. } => (MayaRawShellItemKind::Capture, *range),
};
MayaRawShellItem {
kind,
span,
text_range: word.text_range(),
}
}
pub(crate) fn raw_item_from_light_word(
parse: &impl LightParseLike,
word: &LightWord,
) -> MayaRawShellItem {
let span = word.range();
let _ = parse;
let kind = match word {
LightWord::Flag { .. } => MayaRawShellItemKind::Flag,
LightWord::NumericLiteral { .. } => MayaRawShellItemKind::Numeric,
LightWord::BareWord { .. } => MayaRawShellItemKind::BareWord,
LightWord::QuotedString { .. } => MayaRawShellItemKind::QuotedString,
LightWord::Variable { .. } => MayaRawShellItemKind::Variable,
LightWord::GroupedExpr { .. } => MayaRawShellItemKind::GroupedExpr,
LightWord::BraceList { .. } => MayaRawShellItemKind::BraceList,
LightWord::VectorLiteral { .. } => MayaRawShellItemKind::VectorLiteral,
LightWord::Capture { .. } => MayaRawShellItemKind::Capture,
};
MayaRawShellItem {
kind,
span,
text_range: light_word_text_range(word),
}
}
pub(crate) fn light_word_text_range(word: &LightWord) -> Option<TextRange> {
match word {
LightWord::Flag { text, .. }
| LightWord::NumericLiteral { text, .. }
| LightWord::BareWord { text, .. }
| LightWord::QuotedString { text, .. } => Some(*text),
LightWord::Variable { .. }
| LightWord::GroupedExpr { .. }
| LightWord::BraceList { .. }
| LightWord::VectorLiteral { .. }
| LightWord::Capture { .. } => None,
}
}
pub(crate) fn maya_normalized_command_from_source(
source: SourceView<'_>,
value: mel_sema::NormalizedCommandInvoke,
) -> MayaNormalizedCommand {
MayaNormalizedCommand {
head: source.slice(value.head_range).to_owned(),
head_range: value.head_range,
schema_name: value.schema_name.to_string(),
kind: value.kind,
mode: value.mode,
items: value
.items
.into_iter()
.map(|item| maya_normalized_command_item_from_source(source, item))
.collect(),
span: value.range,
}
}
fn maya_normalized_command_item_from_source(
source: SourceView<'_>,
value: NormalizedCommandItem,
) -> MayaNormalizedCommandItem {
match value {
NormalizedCommandItem::Flag(flag) => {
MayaNormalizedCommandItem::Flag(maya_normalized_flag_from_source(source, flag))
}
NormalizedCommandItem::Positional(arg) => {
MayaNormalizedCommandItem::Positional(maya_positional_arg_from_source(source, arg))
}
}
}
fn maya_normalized_flag_from_source(
source: SourceView<'_>,
value: NormalizedFlag,
) -> MayaNormalizedFlag {
MayaNormalizedFlag {
source_range: value.source_range,
canonical_name: value.canonical_name.map(|name| name.to_string()),
args: value
.args
.into_iter()
.map(|arg| maya_positional_arg_from_source(source, arg))
.collect(),
span: value.range,
}
}
fn maya_positional_arg_from_source(
source: SourceView<'_>,
value: PositionalArg,
) -> MayaPositionalArg {
MayaPositionalArg {
item: raw_item_from_shell_word_with_source(source, &value.word),
}
}
pub(crate) fn command_payload_span(
head_range: TextRange,
raw_items: &[MayaRawShellItem],
) -> TextRange {
let end = raw_items
.last()
.map(|item| range_end(item.span))
.unwrap_or_else(|| range_end(head_range));
text_range(range_start(head_range), end)
}
pub(crate) fn normalize_light_command(
parse: &impl LightParseLike,
head: &str,
head_range: TextRange,
span: TextRange,
schema: &CommandSchema,
items: &[MayaRawShellItem],
) -> MayaNormalizedCommand {
let mode = detect_light_mode(parse, schema, items);
let mut normalized_items = Vec::new();
let mut index = 0;
while index < items.len() {
let item = &items[index];
if item.kind != MayaRawShellItemKind::Flag {
normalized_items.push(MayaNormalizedCommandItem::Positional(MayaPositionalArg {
item: 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(MayaPositionalArg {
item: next_item.clone(),
});
consumed += 1;
}
let item_span = args.last().map_or(item.span, |arg| {
text_range(range_start(item.span), range_end(arg.item.span))
});
normalized_items.push(MayaNormalizedCommandItem::Flag(MayaNormalizedFlag {
source_range: item.span,
canonical_name: schema_flag.as_ref().map(|flag| flag.long_name.to_string()),
args,
span: item_span,
}));
index += 1 + consumed;
}
let normalized_span = normalized_items.last().map_or(span, |item| {
let end = match item {
MayaNormalizedCommandItem::Flag(flag) => range_end(flag.span),
MayaNormalizedCommandItem::Positional(arg) => range_end(arg.item.span),
};
text_range(range_start(head_range), end)
});
MayaNormalizedCommand {
head: head.to_owned(),
head_range,
schema_name: schema.name.to_string(),
kind: schema.kind,
mode,
items: normalized_items,
span: normalized_span,
}
}
fn detect_light_mode(
parse: &impl LightParseLike,
schema: &CommandSchema,
items: &[MayaRawShellItem],
) -> 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) => CommandMode::Create,
(false, true, false) => CommandMode::Edit,
(false, false, true) => CommandMode::Query,
_ => 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: CommandMode) -> FlagArity {
match mode {
CommandMode::Create | CommandMode::Unknown => arity_by_mode.create,
CommandMode::Edit => arity_by_mode.edit,
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)),
}
}