Skip to content

Commit 147ffd0

Browse files
committed
refactor: split precedence.rs into a folder module
1015-line precedence.rs had three cohesive groups: env-var wire format, diff output types with rendering, and the Precedence builder engine. Split into separate files under crates/am/src/precedence/ so each file holds one responsibility and stays a comfortable read. - precedence/mod.rs — module declarations + re-exports only - precedence/env_state.rs — AliasWithHash + AliasWithHashList (~193 lines) - precedence/diff.rs — EntryKind, EffectiveEntry, PrecedenceDiff with change_summary + render (~194 lines) - precedence/engine.rs — Precedence builder + resolve + hashing helpers (~653 lines) Public API is unchanged. All external uses of amoxide::precedence::{Precedence, PrecedenceDiff, EntryKind, EffectiveEntry, AliasWithHash, AliasWithHashList} still resolve. No signatures, behavior, or test count changed.
1 parent 2ba7374 commit 147ffd0

4 files changed

Lines changed: 406 additions & 365 deletions

File tree

crates/am/src/precedence/diff.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
use crate::alias::TomlAlias;
2+
use crate::env_vars;
3+
use crate::shell::ShellAdapter;
4+
use crate::subcommand::SubcommandEntry;
5+
6+
use super::env_state::{AliasWithHash, AliasWithHashList};
7+
8+
#[derive(Debug, Clone, PartialEq)]
9+
pub enum EntryKind {
10+
Alias(TomlAlias),
11+
SubcommandWrapper {
12+
program: String,
13+
entries: Vec<SubcommandEntry>,
14+
base_cmd: Option<String>,
15+
},
16+
/// Per-key subcommand entry tracked in `_AM_SUBCOMMANDS` for fine-grained
17+
/// change detection. Never emitted as shell code — the program-level
18+
/// `SubcommandWrapper` is the shell-visible unit.
19+
SubcommandKey {
20+
longs: Vec<String>,
21+
},
22+
}
23+
24+
#[derive(Debug, Clone, PartialEq)]
25+
pub struct EffectiveEntry {
26+
pub name: String,
27+
pub kind: EntryKind,
28+
pub hash: String,
29+
}
30+
31+
#[derive(Debug, Default, Clone, PartialEq)]
32+
pub struct PrecedenceDiff {
33+
pub added: Vec<EffectiveEntry>,
34+
pub changed: Vec<EffectiveEntry>,
35+
pub removed: Vec<String>,
36+
pub unchanged: Vec<EffectiveEntry>,
37+
}
38+
39+
impl PrecedenceDiff {
40+
/// Human-readable summary of what changed, suitable for echoing to the
41+
/// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`).
42+
///
43+
/// Returns `None` when nothing changed so callers can stay silent.
44+
pub fn change_summary(&self) -> Option<String> {
45+
let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect();
46+
let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect();
47+
let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect();
48+
let parts: Vec<String> = [
49+
("loaded", &added[..]),
50+
("updated", &changed[..]),
51+
("unloaded", &removed[..]),
52+
]
53+
.iter()
54+
.filter(|(_, names)| !names.is_empty())
55+
.map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
56+
.collect();
57+
if parts.is_empty() {
58+
None
59+
} else {
60+
Some(format!("am: aliases changed — {}", parts.join(" | ")))
61+
}
62+
}
63+
64+
/// Render this diff into shell code using the given adapter.
65+
///
66+
/// Emission order:
67+
/// 1. unload (removed + changed) — skipping subcommand-key names
68+
/// (they're tracking-only, not shell functions)
69+
/// 2. load (added + changed)
70+
/// 3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added +
71+
/// changed + unchanged
72+
pub fn render(&self, shell: &dyn ShellAdapter) -> String {
73+
let mut lines: Vec<String> = Vec::new();
74+
75+
// 1. Unload
76+
for name in &self.removed {
77+
if name.contains(':') {
78+
continue;
79+
}
80+
lines.push(shell.unalias(name));
81+
}
82+
for entry in &self.changed {
83+
if matches!(entry.kind, EntryKind::SubcommandKey { .. }) {
84+
continue;
85+
}
86+
if entry.name.contains(':') {
87+
continue;
88+
}
89+
lines.push(shell.unalias(&entry.name));
90+
}
91+
92+
// 2. Load (added + changed)
93+
for entry in self.added.iter().chain(self.changed.iter()) {
94+
match &entry.kind {
95+
EntryKind::Alias(alias) => {
96+
lines.push(shell.alias(&alias.as_entry(&entry.name)));
97+
}
98+
EntryKind::SubcommandWrapper {
99+
program,
100+
entries,
101+
base_cmd,
102+
} => {
103+
let cmd = base_cmd
104+
.clone()
105+
.unwrap_or_else(|| format!("command {program}"));
106+
lines.push(shell.subcommand_wrapper(program, &cmd, entries));
107+
}
108+
EntryKind::SubcommandKey { .. } => {}
109+
}
110+
}
111+
112+
// 3. Update tracking env vars
113+
let mut alias_list = AliasWithHashList::new();
114+
let mut sub_list = AliasWithHashList::new();
115+
for e in self
116+
.added
117+
.iter()
118+
.chain(self.changed.iter())
119+
.chain(self.unchanged.iter())
120+
{
121+
let entry = AliasWithHash::new(&e.name, Some(e.hash.clone()));
122+
match &e.kind {
123+
EntryKind::SubcommandKey { .. } => sub_list.push(entry),
124+
_ => alias_list.push(entry),
125+
}
126+
}
127+
128+
if !alias_list.is_empty() {
129+
lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_list.to_string()));
130+
}
131+
if !sub_list.is_empty() {
132+
lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_list.to_string()));
133+
}
134+
135+
lines.join("\n")
136+
}
137+
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use super::super::engine::Precedence;
142+
use super::*;
143+
use crate::alias::{AliasName, AliasSet};
144+
use crate::config::ShellsTomlConfig;
145+
use crate::shell::Shell;
146+
use crate::subcommand::SubcommandSet;
147+
148+
fn aset(pairs: &[(&str, &str)]) -> AliasSet {
149+
let mut s = AliasSet::default();
150+
for (n, c) in pairs {
151+
s.insert(AliasName::from(*n), TomlAlias::Command((*c).into()));
152+
}
153+
s
154+
}
155+
156+
#[test]
157+
fn render_emits_unloads_then_loads_then_env() {
158+
let cfg = ShellsTomlConfig::default();
159+
let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
160+
161+
// Previous shell state: `b|0000000,gone|aaa` ; new effective: `b|make build`.
162+
let project = aset(&[("b", "make build")]);
163+
let diff = Precedence::new()
164+
.with_project(&project, &SubcommandSet::new())
165+
.with_shell_state_from_env(Some("b|0000000,gone|aaa"), None)
166+
.resolve();
167+
168+
let out = diff.render(shell.as_ref());
169+
assert!(
170+
out.contains("functions -e gone"),
171+
"gone must be unloaded: {out}"
172+
);
173+
assert!(
174+
out.contains("functions -e b"),
175+
"changed b must be unloaded: {out}"
176+
);
177+
assert!(
178+
out.contains("function b\n make build $argv\nend"),
179+
"b must be reloaded: {out}"
180+
);
181+
// env-var update must be the last section
182+
let env_pos = out.find("_AM_ALIASES").expect("env update missing");
183+
let fn_pos = out.find("function b").unwrap();
184+
assert!(env_pos > fn_pos, "env update must come after loads");
185+
}
186+
187+
#[test]
188+
fn render_empty_diff_produces_empty_string() {
189+
let cfg = ShellsTomlConfig::default();
190+
let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
191+
let out = PrecedenceDiff::default().render(shell.as_ref());
192+
assert!(out.is_empty());
193+
}
194+
}

0 commit comments

Comments
 (0)