forge_server/surface/
extensions.rs1use forge_primitives::plugins::{PluginUpdateRun, PluginsInventorySnapshot};
9
10use super::ViewSurface;
11
12pub struct LspServerCheck {
14 pub plugin: String,
16 pub server: String,
17 pub command: String,
20 pub on_path: bool,
21}
22
23pub struct ExtensionsView {
25 pub snapshot: Option<PluginsInventorySnapshot>,
28 pub update_run: Option<PluginUpdateRun>,
30 pub lsp: Vec<LspServerCheck>,
31}
32
33impl ViewSurface {
34 pub async fn extensions(&self) -> ExtensionsView {
35 let snapshot = self.workspace.plugins_inventory().await;
36 let path = std::env::var_os("PATH");
37 let path = path.as_deref().map(|path| path.to_string_lossy().into_owned());
38 let lsp = snapshot
39 .as_ref()
40 .map(|snapshot| lsp_checks(snapshot, path.as_deref()))
41 .unwrap_or_default();
42 ExtensionsView { snapshot, update_run: self.workspace.plugin_update_run(), lsp }
43 }
44}
45
46pub(crate) fn lsp_checks(
51 snapshot: &PluginsInventorySnapshot,
52 path_var: Option<&str>,
53) -> Vec<LspServerCheck> {
54 let mut checks = Vec::new();
55 for components in snapshot.components.iter().filter(|entry| entry.installed) {
56 for (server, command) in &components.lsp_servers {
57 let probed = if command.is_empty() { server.as_str() } else { command.as_str() };
58 checks.push(LspServerCheck {
59 plugin: components.plugin.clone(),
60 server: server.clone(),
61 command: command.clone(),
62 on_path: forge_workspace::env::lsp_path::lsp_binary_on_path(probed, path_var),
63 });
64 }
65 }
66 checks
67}
68
69#[cfg(test)]
70mod tests {
71 use forge_primitives::plugins::{PluginComponents, PluginsInventorySnapshot};
72
73 use super::*;
74
75 fn snapshot_with_lsp(servers: &[(&str, &str)]) -> PluginsInventorySnapshot {
76 let mut components = PluginComponents {
77 plugin: "lsp-plugin@probe-market".to_owned(),
78 installed: true,
79 enabled: true,
80 ..PluginComponents::default()
81 };
82 for (server, command) in servers {
83 components.lsp_servers.insert((*server).to_owned(), (*command).to_owned());
84 }
85 PluginsInventorySnapshot {
86 components: vec![components],
87 ..PluginsInventorySnapshot::default()
88 }
89 }
90
91 #[test]
94 fn the_check_reflects_the_injected_path() {
95 use std::os::unix::fs::PermissionsExt;
96 let dir = tempfile::tempdir().expect("tempdir");
97 let binary = dir.path().join("probe-ls");
98 std::fs::write(&binary, b"#!/bin/sh").expect("write binary");
99 std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)).expect("chmod");
100 let path = dir.path().to_string_lossy().into_owned();
101
102 let snapshot = snapshot_with_lsp(&[("probe-server", "probe-ls"), ("absent", "absent-bin")]);
103 let checks = lsp_checks(&snapshot, Some(&path));
104
105 assert_eq!(checks.len(), 2);
106 assert_eq!(checks[0].server, "absent");
107 assert!(!checks[0].on_path, "a binary not on the injected PATH is reported missing");
108 assert_eq!(checks[1].server, "probe-server");
109 assert_eq!(checks[1].command, "probe-ls");
110 assert!(checks[1].on_path, "the declared command is what was probed");
111
112 assert!(
113 lsp_checks(&snapshot, None).iter().all(|check| !check.on_path),
114 "no PATH at all reports every server missing rather than guessing"
115 );
116 }
117
118 #[test]
123 fn only_installed_plugins_are_probed_and_a_blank_command_falls_back_to_the_key() {
124 use std::os::unix::fs::PermissionsExt;
125 let dir = tempfile::tempdir().expect("tempdir");
126 let binary = dir.path().join("key-named");
127 std::fs::write(&binary, b"#!/bin/sh").expect("write binary");
128 std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)).expect("chmod");
129 let path = dir.path().to_string_lossy().into_owned();
130
131 let mut installed = PluginComponents {
132 plugin: "installed@probe-market".to_owned(),
133 installed: true,
134 enabled: true,
135 ..PluginComponents::default()
136 };
137 installed.lsp_servers.insert("key-named".to_owned(), String::new());
138 let mut gone = PluginComponents {
139 plugin: "gone@probe-market".to_owned(),
140 installed: false,
141 enabled: false,
142 ..PluginComponents::default()
143 };
144 gone.lsp_servers.insert("not-probed".to_owned(), "not-probed".to_owned());
145 let snapshot = PluginsInventorySnapshot {
146 components: vec![installed, gone],
147 ..PluginsInventorySnapshot::default()
148 };
149
150 let checks = lsp_checks(&snapshot, Some(&path));
151
152 assert_eq!(checks.len(), 1, "an uninstalled plugin's servers are not probed");
153 assert_eq!(checks[0].plugin, "installed@probe-market");
154 assert!(
155 checks[0].on_path,
156 "a blank declared command falls back to the server key as the binary"
157 );
158 }
159}