1use vs_core::UseScope;
4use vs_plugin_api::PluginBackendKind;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct ToolRow {
9 pub name: String,
10 pub current_version: Option<String>,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct VersionRow {
16 pub version: String,
17 pub installed: bool,
18 pub current: bool,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ScopeChoice {
24 Project,
25 Global,
26 Session,
27}
28
29impl ScopeChoice {
30 pub fn all() -> [ScopeChoice; 3] {
31 [
32 ScopeChoice::Project,
33 ScopeChoice::Global,
34 ScopeChoice::Session,
35 ]
36 }
37
38 pub fn label(self) -> &'static str {
39 match self {
40 ScopeChoice::Project => "Project",
41 ScopeChoice::Global => "Global",
42 ScopeChoice::Session => "Session",
43 }
44 }
45
46 pub fn to_use_scope(self) -> UseScope {
47 match self {
48 ScopeChoice::Project => UseScope::Project,
49 ScopeChoice::Global => UseScope::Global,
50 ScopeChoice::Session => UseScope::Session,
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum BackendChoice {
58 Lua,
59 Wasi,
60}
61
62impl BackendChoice {
63 pub fn to_kind(self) -> PluginBackendKind {
64 match self {
65 BackendChoice::Lua => PluginBackendKind::Lua,
66 BackendChoice::Wasi => PluginBackendKind::Wasi,
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum AddSource {
74 Registry {
75 name: String,
76 },
77 Source {
78 source: String,
79 alias: Option<String>,
80 backend: BackendChoice,
81 },
82}
83
84pub fn merge_tool_rows(
87 names: Vec<String>,
88 statuses: Vec<(String, Option<String>)>,
89) -> Vec<ToolRow> {
90 let mut rows: Vec<ToolRow> = names
91 .into_iter()
92 .map(|name| {
93 let current_version = statuses
94 .iter()
95 .find(|(plugin, _)| *plugin == name)
96 .and_then(|(_, version)| version.clone());
97 ToolRow {
98 name,
99 current_version,
100 }
101 })
102 .collect();
103 rows.sort_by(|a, b| a.name.cmp(&b.name));
104 rows
105}
106
107pub fn installed_rows(installed: Vec<String>, current: Option<&str>) -> Vec<VersionRow> {
109 installed
110 .into_iter()
111 .map(|version| {
112 let is_current = current == Some(version.as_str());
113 VersionRow {
114 version,
115 installed: true,
116 current: is_current,
117 }
118 })
119 .collect()
120}
121
122pub fn available_rows(found: Vec<String>, installed: &[String]) -> Vec<VersionRow> {
125 found
126 .into_iter()
127 .map(|version| {
128 let already = installed.iter().any(|v| v == &version);
129 VersionRow {
130 version,
131 installed: already,
132 current: false,
133 }
134 })
135 .collect()
136}
137
138pub fn progress_percent(done: u64, total: Option<u64>) -> Option<f32> {
143 match total {
144 Some(total) if total > 0 => Some((done as f32 / total as f32 * 100.0).clamp(0.0, 100.0)),
145 _ => None,
146 }
147}
148
149pub fn filter_tool_rows(rows: &[ToolRow], query: &str) -> Vec<ToolRow> {
151 if query.is_empty() {
152 return rows.to_vec();
153 }
154 let needle = query.to_lowercase();
155 rows.iter()
156 .filter(|row| row.name.to_lowercase().contains(&needle))
157 .cloned()
158 .collect()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn scope_choice_maps_to_use_scope_and_all_lists_three() {
167 assert_eq!(ScopeChoice::Project.to_use_scope(), UseScope::Project);
168 assert_eq!(ScopeChoice::Global.to_use_scope(), UseScope::Global);
169 assert_eq!(ScopeChoice::Session.to_use_scope(), UseScope::Session);
170 assert_eq!(ScopeChoice::all().len(), 3);
171 assert_eq!(ScopeChoice::Project.label(), "Project");
172 }
173
174 #[test]
175 fn backend_choice_maps_to_plugin_backend_kind() {
176 assert_eq!(BackendChoice::Lua.to_kind(), PluginBackendKind::Lua);
177 assert_eq!(BackendChoice::Wasi.to_kind(), PluginBackendKind::Wasi);
178 }
179
180 #[test]
181 fn merge_tool_rows_pairs_names_with_current_versions_sorted() {
182 let names = vec!["python".to_string(), "nodejs".to_string()];
183 let statuses = vec![
184 ("nodejs".to_string(), Some("20.11.1".to_string())),
185 ("python".to_string(), None),
186 ];
187 let rows = merge_tool_rows(names, statuses);
188 assert_eq!(
189 rows,
190 vec![
191 ToolRow {
192 name: "nodejs".into(),
193 current_version: Some("20.11.1".into())
194 },
195 ToolRow {
196 name: "python".into(),
197 current_version: None
198 },
199 ]
200 );
201 }
202
203 #[test]
204 fn installed_rows_flag_the_current_version() {
205 let rows = installed_rows(
206 vec!["20.11.1".to_string(), "18.19.0".to_string()],
207 Some("20.11.1"),
208 );
209 assert_eq!(
210 rows,
211 vec![
212 VersionRow {
213 version: "20.11.1".into(),
214 installed: true,
215 current: true
216 },
217 VersionRow {
218 version: "18.19.0".into(),
219 installed: true,
220 current: false
221 },
222 ]
223 );
224 }
225
226 #[test]
227 fn available_rows_mark_already_installed_versions() {
228 let rows = available_rows(
229 vec!["21.6.0".to_string(), "20.11.1".to_string()],
230 &["20.11.1".to_string()],
231 );
232 assert_eq!(
233 rows,
234 vec![
235 VersionRow {
236 version: "21.6.0".into(),
237 installed: false,
238 current: false
239 },
240 VersionRow {
241 version: "20.11.1".into(),
242 installed: true,
243 current: false
244 },
245 ]
246 );
247 }
248
249 #[test]
250 fn filter_tool_rows_is_case_insensitive_substring() {
251 let rows = vec![
252 ToolRow {
253 name: "nodejs".into(),
254 current_version: None,
255 },
256 ToolRow {
257 name: "python".into(),
258 current_version: None,
259 },
260 ];
261 let filtered = filter_tool_rows(&rows, "PY");
262 assert_eq!(filtered.len(), 1);
263 assert_eq!(filtered[0].name, "python");
264 assert_eq!(filter_tool_rows(&rows, "").len(), 2);
266 }
267
268 #[test]
269 fn progress_percent_computes_clamped_ratio_or_none() {
270 assert_eq!(progress_percent(50, Some(200)), Some(25.0));
271 assert_eq!(progress_percent(200, Some(200)), Some(100.0));
272 assert_eq!(progress_percent(300, Some(200)), Some(100.0));
274 assert_eq!(progress_percent(10, None), None);
276 assert_eq!(progress_percent(10, Some(0)), None);
277 }
278}