use std::collections::HashSet;
use ink_analyzer_ir::{
ast::{self, HasName},
syntax::{AstNode, TextRange},
ChainExtension, Constructor, Contract, ContractRef, InkEntity, InkFile, IsInkEvent, IsInkFn,
IsInkTrait, Message, TraitDefinition, Version,
};
use super::{Action, ActionKind};
use crate::analysis::{
text_edit::{self, TextEdit},
utils,
};
use crate::codegen::snippets::{
CONSTRUCTOR_PLAIN, CONSTRUCTOR_SNIPPET, MESSAGE_PLAIN, MESSAGE_SNIPPET,
};
pub fn add_storage(
contract: &Contract,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
contract.module().and_then(|module| {
range_option
.or_else(|| {
module
.item_list()
.as_ref()
.map(utils::item_insert_offset_start)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! storage `struct`.".to_owned(),
kind,
range: utils::contract_declaration_range(contract),
edits: vec![text_edit::add_storage(contract, range)],
})
})
}
pub fn add_event_v1(
contract: &Contract,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
contract.module().and_then(|module| {
range_option
.or_else(|| {
module
.item_list()
.as_ref()
.map(utils::item_insert_offset_after_last_adt_or_start)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! event `struct`.".to_owned(),
kind,
range: utils::contract_declaration_range(contract),
edits: vec![text_edit::add_event_v1(module, range)],
})
})
}
pub fn add_event_v2(range: TextRange, kind: ActionKind, indent_option: Option<&str>) -> Action {
Action {
label: "Add ink! event 2.0 `struct`.".to_owned(),
kind,
range,
edits: vec![text_edit::add_event_v2(range, indent_option, None)],
}
}
pub fn add_topic<T>(event: &T, kind: ActionKind, range_option: Option<TextRange>) -> Option<Action>
where
T: IsInkEvent,
{
event.struct_item().and_then(|struct_item| {
range_option
.map(|offset| (offset, None, None))
.or_else(|| {
struct_item
.field_list()
.as_ref()
.map(utils::field_insert_offset_end_and_affixes)
.map(|(offset, prefix, suffix)| {
(TextRange::new(offset, offset), prefix, suffix)
})
})
.map(|(range, prefix, suffix)| Action {
label: "Add ink! topic `field`.".to_owned(),
kind,
range: utils::ast_item_declaration_range(&ast::Item::Struct(struct_item.clone()))
.unwrap_or(struct_item.syntax().text_range()),
edits: vec![text_edit::add_topic(
struct_item,
range,
prefix.as_deref(),
suffix.as_deref(),
)],
})
})
}
fn add_callable_to_contract(
contract: &Contract,
kind: ActionKind,
range_option: Option<TextRange>,
label: String,
plain: &str,
snippet: &str,
) -> Option<Action> {
range_option
.and_then(|range| utils::parent_ast_item(contract, range))
.and_then(|it| match it {
ast::Item::Impl(impl_item) => Some(impl_item),
_ => None,
})
.filter(|impl_item| {
contract
.syntax()
.text_range()
.contains_range(impl_item.syntax().text_range())
})
.and_then(|impl_item| {
add_callable_to_impl(
&impl_item,
kind,
range_option,
label.clone(),
plain,
snippet,
)
})
.or_else(|| {
range_option
.zip(utils::callable_impl_indent_and_affixes(contract))
.map(|(insert_offset, (indent, prefix, suffix))| {
(insert_offset, indent, Some(prefix), Some(suffix))
})
.or_else(|| {
utils::callable_insert_offset_indent_and_affixes(contract).map(
|(offset, ident, prefix, suffix)| {
(TextRange::new(offset, offset), ident, prefix, suffix)
},
)
})
.map(|(range, indent, prefix, suffix)| Action {
label,
kind,
range: utils::contract_declaration_range(contract),
edits: vec![TextEdit::replace_with_snippet(
format!(
"{}{}{}",
prefix.as_deref().unwrap_or_default(),
utils::apply_indenting(plain, &indent),
suffix.as_deref().unwrap_or_default()
),
range,
Some(format!(
"{}{}{}",
prefix.as_deref().unwrap_or_default(),
utils::apply_indenting(snippet, &indent),
suffix.as_deref().unwrap_or_default()
)),
)],
})
})
}
fn contract_fn_names(contract: &Contract) -> HashSet<String> {
contract
.constructors()
.iter()
.filter_map(Constructor::fn_item)
.chain(contract.messages().iter().filter_map(Message::fn_item))
.filter_map(|fn_item| fn_item.name().as_ref().map(ToString::to_string))
.collect()
}
pub fn add_constructor_to_contract(
contract: &Contract,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
let names = contract_fn_names(contract);
let (text, snippet) =
text_edit::unique_text_and_snippet(CONSTRUCTOR_PLAIN, CONSTRUCTOR_SNIPPET, "new", &names);
add_callable_to_contract(
contract,
kind,
range_option,
"Add ink! constructor `fn`.".to_owned(),
&text,
&snippet,
)
}
pub fn add_message_to_contract(
contract: &Contract,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
let names = contract_fn_names(contract);
let (text, snippet) =
text_edit::unique_text_and_snippet(MESSAGE_PLAIN, MESSAGE_SNIPPET, "my_message", &names);
add_callable_to_contract(
contract,
kind,
range_option,
"Add ink! message `fn`.".to_owned(),
&text,
&snippet,
)
}
pub fn add_message_selector_to_contract(
contract: &Contract,
kind: ActionKind,
selector: &str,
fn_name: &str,
range_option: Option<TextRange>,
label_option: Option<String>,
) -> Option<Action> {
let names = contract_fn_names(contract);
let (mut text, mut snippet) =
text_edit::unique_text_and_snippet(MESSAGE_PLAIN, MESSAGE_SNIPPET, fn_name, &names);
let selector_attr = format!("#[ink(message, selector = {selector})]");
text = text.replace("#[ink(message)]", &selector_attr);
snippet = snippet.replace("#[ink(message)]", &selector_attr);
add_callable_to_contract(
contract,
kind,
range_option,
label_option
.unwrap_or_else(|| format!("Add ink! message `fn` with `selector = {selector}`.")),
&text,
&snippet,
)
}
fn add_callable_to_impl(
impl_item: &ast::Impl,
kind: ActionKind,
range_option: Option<TextRange>,
label: String,
plain: &str,
snippet: &str,
) -> Option<Action> {
range_option
.or_else(|| {
impl_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| {
let indent = utils::item_children_indenting(impl_item.syntax());
Action {
label,
kind,
range: utils::ast_item_declaration_range(&ast::Item::Impl(impl_item.clone()))
.unwrap_or(impl_item.syntax().text_range()),
edits: vec![TextEdit::replace_with_snippet(
utils::apply_indenting(plain, &indent),
range,
Some(utils::apply_indenting(snippet, &indent)),
)],
}
})
}
pub fn add_constructor_to_impl(
impl_item: &ast::Impl,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
range_option
.or_else(|| {
impl_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! constructor `fn`.".to_owned(),
kind,
range: utils::ast_item_declaration_range(&ast::Item::Impl(impl_item.clone()))
.unwrap_or(impl_item.syntax().text_range()),
edits: vec![text_edit::add_constructor_to_impl(impl_item, range)],
})
}
pub fn add_message_to_impl(
impl_item: &ast::Impl,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
range_option
.or_else(|| {
impl_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! message `fn`.".to_owned(),
kind,
range: utils::ast_item_declaration_range(&ast::Item::Impl(impl_item.clone()))
.unwrap_or(impl_item.syntax().text_range()),
edits: vec![text_edit::add_message_to_impl(impl_item, range)],
})
}
pub fn add_message_to_trait_def(
trait_definition: &TraitDefinition,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
trait_definition.trait_item().and_then(|trait_item| {
range_option
.or_else(|| {
trait_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! message `fn`.".to_owned(),
kind,
range: utils::ink_trait_declaration_range(trait_definition),
edits: vec![text_edit::add_message_to_trait_def(trait_definition, range)],
})
})
}
pub fn add_message_to_contract_ref(
contract_ref: &ContractRef,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
contract_ref.trait_item().and_then(|trait_item| {
range_option
.or_else(|| {
trait_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! message `fn`.".to_owned(),
kind,
range: utils::ink_trait_declaration_range(contract_ref),
edits: vec![text_edit::add_message_to_contract_ref(contract_ref, range)],
})
})
}
pub fn add_error_code(
chain_extension: &ChainExtension,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
chain_extension.trait_item().and_then(|trait_item| {
range_option
.or_else(|| {
trait_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_start)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add `ErrorCode` type for ink! chain extension.".to_owned(),
kind,
range: utils::ink_trait_declaration_range(chain_extension),
edits: vec![text_edit::add_error_code_type(chain_extension, range)],
})
})
}
pub fn add_extension(
chain_extension: &ChainExtension,
kind: ActionKind,
range_option: Option<TextRange>,
version: Version,
) -> Option<Action> {
chain_extension.trait_item().and_then(|trait_item| {
range_option
.or_else(|| {
trait_item
.assoc_item_list()
.as_ref()
.map(utils::assoc_item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| {
Action {
label: format!(
"Add ink! {} `fn`.",
if version.is_legacy() {
"extension"
} else {
"function"
}
),
kind,
range: utils::ink_trait_declaration_range(chain_extension),
edits: vec![text_edit::add_extension(chain_extension, range, version)],
}
})
})
}
pub fn add_ink_test(
module: &ast::Module,
kind: ActionKind,
range_option: Option<TextRange>,
) -> Option<Action> {
range_option
.or_else(|| {
module
.item_list()
.as_ref()
.map(utils::item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! test `fn`.".to_owned(),
kind,
range: utils::ast_item_declaration_range(&ast::Item::Module(module.clone()))
.unwrap_or(module.syntax().text_range()),
edits: vec![text_edit::add_test(module, range)],
})
}
pub fn add_ink_e2e_test(
module: &ast::Module,
kind: ActionKind,
range_option: Option<TextRange>,
version: Version,
) -> Option<Action> {
range_option
.or_else(|| {
module
.item_list()
.as_ref()
.map(utils::item_insert_offset_end)
.map(|offset| TextRange::new(offset, offset))
})
.map(|range| Action {
label: "Add ink! e2e test `fn`.".to_owned(),
kind,
range: utils::ast_item_declaration_range(&ast::Item::Module(module.clone()))
.unwrap_or(module.syntax().text_range()),
edits: vec![text_edit::add_e2e_test(module, range, version)],
})
}
pub fn add_contract(
range: TextRange,
kind: ActionKind,
indent_option: Option<&str>,
version: Version,
) -> Action {
Action {
label: "Add ink! contract `mod`.".to_owned(),
kind,
range,
edits: vec![text_edit::add_contract(range, indent_option, version)],
}
}
pub fn add_contract_ref(range: TextRange, kind: ActionKind, indent_option: Option<&str>) -> Action {
Action {
label: "Add ink! contract reference.".to_owned(),
kind,
range,
edits: vec![text_edit::add_contract_ref(range, indent_option)],
}
}
pub fn add_trait_definition(
range: TextRange,
kind: ActionKind,
indent_option: Option<&str>,
) -> Action {
Action {
label: "Add ink! trait definition.".to_owned(),
kind,
range,
edits: vec![text_edit::add_trait_def(range, indent_option)],
}
}
pub fn add_chain_extension(
range: TextRange,
kind: ActionKind,
indent_option: Option<&str>,
version: Version,
) -> Action {
Action {
label: "Add ink! chain extension `trait`.".to_owned(),
kind,
range,
edits: vec![text_edit::add_chain_extension(
range,
indent_option,
version,
)],
}
}
pub fn add_combine_extensions(
range: TextRange,
kind: ActionKind,
indent_option: Option<&str>,
file: Option<&InkFile>,
) -> Action {
Action {
label: "Add ink! combine extensions definition.".to_owned(),
kind,
range,
edits: vec![text_edit::add_combine_extensions(
range,
indent_option,
file,
)],
}
}
pub fn add_storage_item(range: TextRange, kind: ActionKind, indent_option: Option<&str>) -> Action {
Action {
label: "Add ink! storage item `ADT` (i.e. `struct`, `enum` or `union`).".to_owned(),
kind,
range,
edits: vec![text_edit::add_storage_item(range, indent_option)],
}
}
pub fn add_environment(
range: TextRange,
kind: ActionKind,
indent_option: Option<&str>,
version: Version,
) -> Action {
Action {
label: "Add custom ink! environment implementation.".to_owned(),
kind,
range,
edits: vec![text_edit::add_environment(range, indent_option, version)],
}
}
pub fn add_error_enum(range: TextRange, kind: ActionKind, indent_option: Option<&str>) -> Action {
Action {
label: "Add ink! error `enum`.".to_owned(),
kind,
range,
edits: vec![text_edit::add_error_enum(range, indent_option)],
}
}
pub fn add_error_struct(range: TextRange, kind: ActionKind, indent_option: Option<&str>) -> Action {
Action {
label: "Add ink! error `struct`.".to_owned(),
kind,
range,
edits: vec![text_edit::add_error_struct(range, indent_option)],
}
}