summer_lsp/scanner/
plugin.rs1use crate::protocol::types::{LocationResponse, PositionResponse, RangeResponse};
6
7use lsp_types::Url;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::Path;
11
12pub struct PluginScanner;
14
15impl PluginScanner {
16 pub fn new() -> Self {
18 Self
19 }
20
21 pub fn scan_plugins(&self, project_path: &Path) -> Result<Vec<PluginInfoResponse>, ScanError> {
31 let mut plugins = Vec::new();
32
33 let main_path = project_path.join("src/main.rs");
35 if !main_path.exists() {
36 let lib_path = project_path.join("src/lib.rs");
38 if !lib_path.exists() {
39 return Err(ScanError::InvalidProject(
40 "Neither main.rs nor lib.rs found".to_string(),
41 ));
42 }
43 self.scan_file(&lib_path, &mut plugins)?;
44 return Ok(plugins);
45 }
46
47 self.scan_file(&main_path, &mut plugins)?;
48 Ok(plugins)
49 }
50
51 fn scan_file(
53 &self,
54 file_path: &Path,
55 plugins: &mut Vec<PluginInfoResponse>,
56 ) -> Result<(), ScanError> {
57 let content = fs::read_to_string(file_path)?;
58 let file_url = Url::from_file_path(file_path)
59 .map_err(|_| ScanError::InvalidProject("Failed to convert path to URL".to_string()))?;
60
61 for (line_num, line) in content.lines().enumerate() {
64 if line.contains(".add_plugin(") {
65 if let Some(plugin_name) = self.extract_plugin_name(line) {
67 plugins.push(PluginInfoResponse {
68 name: plugin_name.clone(),
69 type_name: plugin_name,
70 config_prefix: None, location: LocationResponse {
72 uri: file_url.to_string(),
73 range: RangeResponse {
74 start: PositionResponse {
75 line: line_num as u32,
76 character: 0,
77 },
78 end: PositionResponse {
79 line: line_num as u32,
80 character: line.len() as u32,
81 },
82 },
83 },
84 });
85 }
86 }
87 }
88
89 Ok(())
90 }
91
92 fn extract_plugin_name(&self, line: &str) -> Option<String> {
94 if let Some(start) = line.find(".add_plugin(") {
96 let after = &line[start + 12..]; let trimmed = after.trim_start();
100
101 let end = trimmed
103 .find(|c: char| c == '(' || c == ')' || c == ',' || c.is_whitespace())
104 .unwrap_or(trimmed.len());
105
106 let plugin_name = &trimmed[..end];
107 if !plugin_name.is_empty() {
108 return Some(plugin_name.to_string());
109 }
110 }
111 None
112 }
113}
114
115impl Default for PluginScanner {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct PluginInfoResponse {
124 pub name: String,
126 #[serde(rename = "typeName")]
128 pub type_name: String,
129 #[serde(rename = "configPrefix")]
131 pub config_prefix: Option<String>,
132 pub location: LocationResponse,
134}
135
136#[derive(Debug, Deserialize)]
138pub struct PluginsRequest {
139 #[serde(rename = "appPath")]
141 pub app_path: String,
142}
143
144#[derive(Debug, Serialize)]
146pub struct PluginsResponse {
147 pub plugins: Vec<PluginInfoResponse>,
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum ScanError {
154 #[error("Failed to read file: {0}")]
155 FileRead(#[from] std::io::Error),
156
157 #[error("Invalid project structure: {0}")]
158 InvalidProject(String),
159
160 #[error("No plugins found")]
161 NoPlugins,
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_plugin_scanner_new() {
170 let _scanner = PluginScanner::new();
171 }
173
174 #[test]
175 fn test_extract_plugin_name() {
176 let scanner = PluginScanner::new();
177
178 assert_eq!(
179 scanner.extract_plugin_name(" .add_plugin(WebPlugin)"),
180 Some("WebPlugin".to_string())
181 );
182
183 assert_eq!(
184 scanner.extract_plugin_name(".add_plugin(SqlxPlugin::new())"),
185 Some("SqlxPlugin::new".to_string())
186 );
187 }
188}