use crate::protocol::types::{LocationResponse, PositionResponse, RangeResponse};
use lsp_types::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
pub struct PluginScanner;
impl PluginScanner {
pub fn new() -> Self {
Self
}
pub fn scan_plugins(&self, project_path: &Path) -> Result<Vec<PluginInfoResponse>, ScanError> {
let mut plugins = Vec::new();
let main_path = project_path.join("src/main.rs");
if !main_path.exists() {
let lib_path = project_path.join("src/lib.rs");
if !lib_path.exists() {
return Err(ScanError::InvalidProject(
"Neither main.rs nor lib.rs found".to_string(),
));
}
self.scan_file(&lib_path, &mut plugins)?;
return Ok(plugins);
}
self.scan_file(&main_path, &mut plugins)?;
Ok(plugins)
}
fn scan_file(
&self,
file_path: &Path,
plugins: &mut Vec<PluginInfoResponse>,
) -> Result<(), ScanError> {
let content = fs::read_to_string(file_path)?;
let file_url = Url::from_file_path(file_path)
.map_err(|_| ScanError::InvalidProject("Failed to convert path to URL".to_string()))?;
for (line_num, line) in content.lines().enumerate() {
if line.contains(".add_plugin(") {
if let Some(plugin_name) = self.extract_plugin_name(line) {
plugins.push(PluginInfoResponse {
name: plugin_name.clone(),
type_name: plugin_name,
config_prefix: None, location: LocationResponse {
uri: file_url.to_string(),
range: RangeResponse {
start: PositionResponse {
line: line_num as u32,
character: 0,
},
end: PositionResponse {
line: line_num as u32,
character: line.len() as u32,
},
},
},
});
}
}
}
Ok(())
}
fn extract_plugin_name(&self, line: &str) -> Option<String> {
if let Some(start) = line.find(".add_plugin(") {
let after = &line[start + 12..];
let trimmed = after.trim_start();
let end = trimmed
.find(|c: char| c == '(' || c == ')' || c == ',' || c.is_whitespace())
.unwrap_or(trimmed.len());
let plugin_name = &trimmed[..end];
if !plugin_name.is_empty() {
return Some(plugin_name.to_string());
}
}
None
}
}
impl Default for PluginScanner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfoResponse {
pub name: String,
#[serde(rename = "typeName")]
pub type_name: String,
#[serde(rename = "configPrefix")]
pub config_prefix: Option<String>,
pub location: LocationResponse,
}
#[derive(Debug, Deserialize)]
pub struct PluginsRequest {
#[serde(rename = "appPath")]
pub app_path: String,
}
#[derive(Debug, Serialize)]
pub struct PluginsResponse {
pub plugins: Vec<PluginInfoResponse>,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("Failed to read file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
#[error("No plugins found")]
NoPlugins,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_scanner_new() {
let _scanner = PluginScanner::new();
}
#[test]
fn test_extract_plugin_name() {
let scanner = PluginScanner::new();
assert_eq!(
scanner.extract_plugin_name(" .add_plugin(WebPlugin)"),
Some("WebPlugin".to_string())
);
assert_eq!(
scanner.extract_plugin_name(".add_plugin(SqlxPlugin::new())"),
Some("SqlxPlugin::new".to_string())
);
}
}