Skip to main content

vs_app/
service.rs

1//! Typed, blocking wrappers around `vs_core::App`.
2
3use std::sync::Arc;
4
5use vs_core::{App, CoreError, InstalledVersion, UninstallResult};
6
7use crate::model::{
8    AddSource, ScopeChoice, ToolRow, VersionRow, available_rows, installed_rows, merge_tool_rows,
9};
10
11/// Blocking, UI-agnostic facade over `vs_core::App`.
12///
13/// Methods block (network/disk I/O) and MUST be called off the UI thread — the
14/// view layer runs them on gpui's background executor.
15#[derive(Clone)]
16pub struct AppService {
17    core: Arc<App>,
18}
19
20impl AppService {
21    pub fn new(core: App) -> Self {
22        Self {
23            core: Arc::new(core),
24        }
25    }
26
27    /// Sidebar: added tools with their current version (active scope).
28    pub fn tool_rows(&self) -> Result<Vec<ToolRow>, CoreError> {
29        let names = self
30            .core
31            .added_plugins()?
32            .into_iter()
33            .map(|entry| entry.name)
34            .collect::<Vec<_>>();
35        let statuses = self.core.current_tool_statuses()?;
36        Ok(merge_tool_rows(names, statuses))
37    }
38
39    /// Detail/Installed: installed versions for a tool with the current flag.
40    pub fn installed_rows(&self, name: &str) -> Result<Vec<VersionRow>, CoreError> {
41        let installed = self
42            .core
43            .installed_versions_for_plugin(name)?
44            .into_iter()
45            .map(|v| v.version)
46            .collect::<Vec<_>>();
47        let current = self.core.current_tool(name)?.map(|c| c.version);
48        Ok(installed_rows(installed, current.as_deref()))
49    }
50
51    /// Detail/Available: search results, marking already-installed versions.
52    pub fn search_available(&self, name: &str, query: &str) -> Result<Vec<VersionRow>, CoreError> {
53        let args: Vec<String> = if query.is_empty() {
54            Vec::new()
55        } else {
56            vec![query.to_string()]
57        };
58        let found = self
59            .core
60            .search_versions(name, &args)?
61            .into_iter()
62            .map(|v| v.version)
63            .collect::<Vec<_>>();
64        let installed = self
65            .core
66            .installed_versions_for_plugin(name)?
67            .into_iter()
68            .map(|v| v.version)
69            .collect::<Vec<_>>();
70        Ok(available_rows(found, &installed))
71    }
72
73    /// Install a specific version, reporting download progress via `on_progress`
74    /// as `(downloaded_bytes, total_bytes_if_known)`.
75    pub fn install_with_progress(
76        &self,
77        name: &str,
78        version: &str,
79        on_progress: &vs_core::ProgressFn<'_>,
80    ) -> Result<InstalledVersion, CoreError> {
81        self.core
82            .install_plugin_version(name, Some(version), Some(on_progress))
83    }
84
85    /// Switch the active version for a tool in the given scope.
86    pub fn use_version(
87        &self,
88        name: &str,
89        version: &str,
90        scope: ScopeChoice,
91    ) -> Result<InstalledVersion, CoreError> {
92        self.core
93            .use_tool(name, version, scope.to_use_scope(), false)
94    }
95
96    /// Uninstall a specific version.
97    pub fn uninstall(&self, name: &str, version: &str) -> Result<UninstallResult, CoreError> {
98        self.core.uninstall_plugin_version(name, version)
99    }
100
101    /// Registry plugins available to add (names).
102    pub fn registry_plugin_names(&self) -> Result<Vec<String>, CoreError> {
103        Ok(self
104            .core
105            .available_plugins()?
106            .into_iter()
107            .map(|entry| entry.name)
108            .collect())
109    }
110
111    /// Add a tool, from the registry or from a source URL/path.
112    pub fn add(&self, source: AddSource) -> Result<(), CoreError> {
113        match source {
114            AddSource::Registry { name } => {
115                self.core.add_plugin(Some(&name), None, None, None)?;
116            }
117            AddSource::Source {
118                source,
119                alias,
120                backend,
121            } => {
122                self.core.add_plugin(
123                    None,
124                    Some(source),
125                    Some(backend.to_kind()),
126                    alias.as_deref(),
127                )?;
128            }
129        }
130        Ok(())
131    }
132
133    /// Update a single plugin to the latest registry definition.
134    /// The refreshed `RegistryEntry` is discarded; the UI only needs success/failure.
135    pub fn update_plugin(&self, name: &str) -> Result<(), CoreError> {
136        self.core.update_plugin(name)?;
137        Ok(())
138    }
139
140    /// Remove a plugin (and its installed SDKs). Returns whether it existed.
141    pub fn remove_plugin(&self, name: &str) -> Result<bool, CoreError> {
142        self.core.remove_plugin(name)
143    }
144
145    /// Refresh the registry index; returns the number of plugins indexed.
146    pub fn refresh_registry(&self) -> Result<usize, CoreError> {
147        self.core.update_registry()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    // The GUI shares `AppService` across gpui's background executor, which
156    // requires the wrapped `App` to be Send + Sync. This is a compile-time guard:
157    // if `vs_core::App` ever stops being thread-safe, this fails to compile and
158    // tells us the worker-thread design must change.
159    #[test]
160    fn app_service_is_send_and_sync() {
161        fn assert_send_sync<T: Send + Sync>() {}
162        assert_send_sync::<AppService>();
163    }
164}