mod apply;
mod bulk_replace;
mod change_signature;
mod delete_readiness;
mod edit_symbol;
mod move_file;
mod move_symbol;
mod organize_imports;
mod rename_related;
mod rename_symbol;
mod signature;
use crate::contract;
use crate::token::TokenStore;
use blazingly_json::{Value, json};
use weavatrix_rust::RepositoryState;
pub struct RefactorSession {
tokens: TokenStore,
write_allowed: bool,
}
impl RefactorSession {
#[must_use]
pub fn new(write_allowed: bool) -> Self {
Self {
tokens: TokenStore::default(),
write_allowed,
}
}
#[must_use]
pub fn read_only() -> Self {
Self::new(false)
}
pub fn call(
&self,
state: &RepositoryState,
name: &str,
arguments: &Value,
) -> Result<Value, String> {
let Some(operation) = Operation::from_name(name) else {
return Err(format!("unknown refactor operation: {name}"));
};
Ok(match operation {
Operation::DeleteReadiness => delete_readiness::delete_readiness(state, arguments),
Operation::EditSymbol => edit_symbol::edit_symbol(state, arguments),
Operation::BulkReplace => bulk_replace::bulk_replace(state, arguments),
Operation::MoveSymbol => move_symbol::move_symbol(state, arguments),
Operation::MoveFile => move_file::move_file(state, arguments),
Operation::OrganizeImports => organize_imports::organize_imports(state, arguments),
Operation::RenameSymbol => {
rename_symbol::rename_symbol(state, &self.tokens, arguments, self.write_allowed)
}
Operation::RenameRelatedSymbols => rename_related::rename_related_symbols(
state,
&self.tokens,
arguments,
self.write_allowed,
),
Operation::ApplyEditPlan => {
apply::apply_edit_plan(state.root(), &self.tokens, arguments, self.write_allowed)
}
Operation::RollbackLastApply => {
apply::rollback_last_apply(state.root(), self.write_allowed)
}
Operation::ChangeSignature => change_signature::change_signature(state, arguments),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operation {
RenameSymbol,
RenameRelatedSymbols,
ApplyEditPlan,
RollbackLastApply,
ChangeSignature,
EditSymbol,
BulkReplace,
OrganizeImports,
MoveFile,
MoveSymbol,
DeleteReadiness,
}
impl Operation {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"rename_symbol" => Some(Self::RenameSymbol),
"rename_related_symbols" => Some(Self::RenameRelatedSymbols),
"apply_edit_plan" => Some(Self::ApplyEditPlan),
"rollback_last_apply" => Some(Self::RollbackLastApply),
"change_signature" => Some(Self::ChangeSignature),
"edit_symbol" => Some(Self::EditSymbol),
"bulk_replace" => Some(Self::BulkReplace),
"organize_imports" => Some(Self::OrganizeImports),
"move_file" => Some(Self::MoveFile),
"move_symbol" => Some(Self::MoveSymbol),
"delete_readiness" => Some(Self::DeleteReadiness),
_ => None,
}
}
#[must_use]
pub const fn writes(self) -> bool {
matches!(
self,
Self::RenameSymbol
| Self::RenameRelatedSymbols
| Self::ApplyEditPlan
| Self::RollbackLastApply
)
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::RenameSymbol => "rename_symbol",
Self::RenameRelatedSymbols => "rename_related_symbols",
Self::ApplyEditPlan => "apply_edit_plan",
Self::RollbackLastApply => "rollback_last_apply",
Self::ChangeSignature => "change_signature",
Self::EditSymbol => "edit_symbol",
Self::BulkReplace => "bulk_replace",
Self::OrganizeImports => "organize_imports",
Self::MoveFile => "move_file",
Self::MoveSymbol => "move_symbol",
Self::DeleteReadiness => "delete_readiness",
}
}
}
#[must_use]
pub fn catalog() -> Value {
contract::catalog_value().clone()
}
#[must_use]
pub fn catalog_names() -> Vec<String> {
contract::tools()
.iter()
.map(|tool| tool.name.clone())
.collect()
}
pub fn call(state: &RepositoryState, name: &str, arguments: &Value) -> Result<Value, String> {
RefactorSession::read_only().call(state, name, arguments)
}
pub(crate) fn not_found(graph: &weavatrix_graph::Graph, symbol: &str) -> Value {
let candidates = crate::resolve::candidate_ids(graph, symbol);
json!({
"status": "NOT_FOUND",
"reason": if candidates.len() > 1 {
"the name matches more than one symbol; pass one of the candidate ids"
} else {
"the selected symbol is not present in the active graph; pass an exact id"
},
"symbol": symbol,
"candidates": candidates,
})
}
pub(crate) fn stale_graph(file: &str) -> Value {
json!({
"status": "STALE_GRAPH",
"reason": format!(
"{file}: the recorded source range no longer matches the file. Rebuild the graph; \
nothing was planned from a range that cannot be located."
),
})
}
pub(crate) fn invalid_args(operation: &str, missing: &[&str]) -> Value {
json!({
"status": "INVALID_ARGS",
"operation": operation,
"invalid": missing,
"reason": format!(
"missing or invalid required argument(s): {}. Nothing was planned or written.",
missing.join(", ")
),
})
}
#[cfg(test)]
mod tests {
use super::{Operation, call, catalog, catalog_names};
use crate::contract;
#[test]
fn every_contract_tool_resolves_to_an_operation() {
for tool in contract::tools() {
assert!(
Operation::from_name(&tool.name).is_some(),
"{} is in the contract with no operation arm",
tool.name
);
}
}
#[test]
fn no_operation_exists_outside_the_contract() {
for name in catalog_names() {
assert!(contract::declares(&name));
}
assert_eq!(catalog_names().len(), contract::tools().len());
}
#[test]
fn exactly_four_operations_can_write() {
let writers = contract::tools()
.iter()
.filter_map(|tool| Operation::from_name(&tool.name))
.filter(|operation| operation.writes())
.count();
assert_eq!(writers, 4);
}
#[test]
fn every_operation_answers_with_a_contract_status_ported_or_not() {
let state = crate::test_support::fixture_state();
for tool in contract::tools() {
let answer =
call(&state, &tool.name, &blazingly_json::json!({})).expect("declared tool");
let status = answer
.get("status")
.and_then(|value| value.as_str())
.unwrap_or_default();
assert!(
contract::permits_state(status),
"{} answered {status}, which is outside the contract",
tool.name
);
}
}
#[test]
fn an_undeclared_tool_is_an_error_not_a_status() {
let state = crate::test_support::fixture_state();
assert!(call(&state, "reformat_universe", &blazingly_json::json!({})).is_err());
}
#[test]
fn the_catalog_matches_the_contract() {
assert_eq!(
catalog().as_array().map(Vec::len),
Some(contract::tools().len())
);
}
}