diff --git a/kclvm/Cargo.lock b/kclvm/Cargo.lock index fb918b5f1..9a567b372 100644 --- a/kclvm/Cargo.lock +++ b/kclvm/Cargo.lock @@ -1532,6 +1532,7 @@ dependencies = [ "threadpool", "tokio", "tokio-test", + "walkdir", ] [[package]] diff --git a/kclvm/tools/src/LSP/Cargo.toml b/kclvm/tools/src/LSP/Cargo.toml index 55c115b4a..0364a46c4 100644 --- a/kclvm/tools/src/LSP/Cargo.toml +++ b/kclvm/tools/src/LSP/Cargo.toml @@ -20,6 +20,7 @@ im-rc = "15.0.0" rustc_lexer = "0.1.0" clap = "4.3.0" maplit = "1.0.2" +walkdir = "2" kclvm-tools = { path = "../../../tools" } kclvm-error = { path = "../../../error" } diff --git a/kclvm/tools/src/LSP/src/config_manager.rs b/kclvm/tools/src/LSP/src/config_manager.rs new file mode 100644 index 000000000..2c4d2b645 --- /dev/null +++ b/kclvm/tools/src/LSP/src/config_manager.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Clone)] +struct JsonFile { + watch: PathBuf, + recursive: Option, + patterns: Option>, +} + +#[derive(Debug, Clone)] +pub struct Config { + path: PathBuf, + recursive: bool, + patterns: Vec, +} + +impl Config { + // Load configuration from file + pub fn load_from_file(file_path: &PathBuf) -> Result> { + let file_content = std::fs::read_to_string(file_path)?; + let config: JsonFile = serde_json::from_str(&file_content)?; + Ok(Config { + path: config.watch, + recursive: config.recursive.unwrap_or(false), + patterns: config.patterns.unwrap_or_default(), + }) + } + + // Get the path from configuration + pub fn path(&self) -> &PathBuf { + &self.path + } + + // Check if the configuration is recursive + pub fn is_recursive(&self) -> bool { + self.recursive + } + + // Get the file patterns from configuration + pub fn patterns(&self) -> &Vec { + &self.patterns + } +} + +// Get the configuration file path +pub fn get_config_file() -> Option { + let current_dir = std::env::current_dir().ok()?; + let config_path = current_dir.join("observer.json"); + + if config_path.exists() { + Some(config_path) + } else { + None + } +} diff --git a/kclvm/tools/src/LSP/src/file.rs b/kclvm/tools/src/LSP/src/file.rs new file mode 100644 index 000000000..211b9f54d --- /dev/null +++ b/kclvm/tools/src/LSP/src/file.rs @@ -0,0 +1,174 @@ +use crate::config_manager::Config; +use std::collections::HashMap; +use std::fs::Metadata; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use std::time::SystemTime; +use walkdir::DirEntry; +use walkdir::WalkDir; + +// Define a trait for file handlers +pub trait FileHandler: Send + Sync { + fn handle(&self, file: &File); +} + +// File structure to hold file metadata and data +#[derive(Debug, Clone, PartialEq)] +pub struct File { + name: String, + path: PathBuf, + data: FileData, +} + +// Data structure for file metadata +#[derive(Debug, Clone, PartialEq)] +struct FileData { + last_accesed: SystemTime, + last_modified: SystemTime, +} + +impl File { + // Create a new File instance from a DirEntry + pub fn new(file: &DirEntry) -> Self { + let metadata = file.metadata().unwrap(); + File { + name: file.file_name().to_str().unwrap().to_string(), + path: file.path().to_path_buf(), + data: FileData::new(metadata), + } + } + + // Get the file name + pub fn name(&self) -> String { + Arc::new(&self.name).to_string() + } + + // Get the file extension + pub fn extension(&self) -> Option { + self.path + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + } + + // Get the display path of the file + pub fn ds_path(&self) -> String { + Arc::new(&self.path) + .to_path_buf() + .to_str() + .unwrap() + .to_string() + } + + // Check if the file was deleted + pub fn was_deleted(&self) -> bool { + !self.path.exists() + } + + // Get the last modification time of the file + pub fn last_modification(&self) -> SystemTime { + self.data.last_modified + } + + // Set the last modification time of the file + pub fn set_modification(&mut self, time: SystemTime) { + self.data.last_modified = time; + } + + // Detect file type based on extension + pub fn detect_file_type(&self) -> Option { + self.extension().map(|ext| { + match ext.as_str() { + "k" => "K File", + "mod" => "Mod File", + "JSON" | "json" => "JSON File", + "YAML" | "yaml" => "YAML File", + _ => "Unknown File Type", + } + .to_string() + }) + } +} + +impl FileData { + // Create a new FileData instance from Metadata + pub fn new(metadata: Metadata) -> Self { + FileData { + last_accesed: metadata.accessed().unwrap(), + last_modified: metadata.modified().unwrap(), + } + } +} + +// Define file events +#[derive(Debug)] +pub enum FileEvent { + Modified(File), +} + +// Observer structure to watch files +#[derive(Debug)] +pub struct Observer { + config: Config, + files: HashMap, +} + +impl Observer { + // Initialize a new Observer instance with a configuration + pub fn new(config: Config) -> Self { + Observer { + files: get_files(&config), + config, + } + } + + // Iterator for file events + pub fn iter_events(&mut self) -> impl Iterator + '_ { + let interval = Duration::from_millis(500); + let last_files = self.files.clone(); + std::iter::from_fn(move || { + let current_files = get_files(&self.config); + + let mut events = Vec::new(); + for (name, file) in current_files.iter() { + if let Some(last_file) = last_files.get(name) { + if file.last_modification() > last_file.last_modification() { + events.push(FileEvent::Modified(file.clone())); + } + } + } + std::thread::sleep(interval); + if !events.is_empty() { + self.files = current_files; + Some(events.remove(0)) + } else { + None + } + }) + } +} + +// Get files based on configuration +fn get_files(config: &Config) -> HashMap { + let files = match config.is_recursive() { + true => WalkDir::new(config.path()).min_depth(1), + false => WalkDir::new(config.path()).min_depth(1).max_depth(1), + } + .into_iter() + .filter(|x| x.as_ref().unwrap().metadata().unwrap().is_file()) + .map(|x| File::new(&x.unwrap())) + .map(|f| (f.name(), f)) + .collect::>(); + + if config.patterns().is_empty() { + return files; + } + let mut filtered_files = HashMap::new(); + for (name, file) in files { + let ext = file.extension().unwrap_or_default(); + if config.patterns().contains(&ext) { + filtered_files.insert(name, file); + } + } + filtered_files +} diff --git a/kclvm/tools/src/LSP/src/kcl_watch_system.rs b/kclvm/tools/src/LSP/src/kcl_watch_system.rs new file mode 100644 index 000000000..56a28b1b6 --- /dev/null +++ b/kclvm/tools/src/LSP/src/kcl_watch_system.rs @@ -0,0 +1,77 @@ +use crate::config_manager::Config; +use crate::file::{FileEvent, FileHandler, Observer}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; + +// HandlerRegistry to register and manage file handlers +pub struct HandlerRegistry { + handlers: HashMap>, +} + +impl HandlerRegistry { + // Create a new HandlerRegistry instance + pub fn new() -> Self { + HandlerRegistry { + handlers: HashMap::new(), + } + } + + // Register a handler for a file type + pub fn register_handler(&mut self, file_type: &str, handler: Box) { + self.handlers.insert(file_type.to_string(), handler); + } + + // Get a handler for a file type + pub fn get_handler(&self, file_type: &str) -> Option<&Box> { + self.handlers.get(file_type) + } + + // Handle file event + pub fn handle_event(&self, event: &FileEvent) { + match event { + FileEvent::Modified(file) => { + if let Some(handler) = + self.get_handler(&file.detect_file_type().unwrap_or_default()) + { + handler.handle(file); + } + } + } + } +} + +// KCL Watch System structure to manage the observer and handler registry +pub struct KCLWatchSystem { + observer: Arc>, + handler_registry: Arc>, +} + +impl KCLWatchSystem { + // Create a new KCL Watch System instance with a configuration + pub fn new(config: Config) -> Self { + let observer = Arc::new(Mutex::new(Observer::new(config.clone()))); + let handler_registry = Arc::new(Mutex::new(HandlerRegistry::new())); + KCLWatchSystem { + observer, + handler_registry, + } + } + + // Start the observer + pub fn start_observer(self) { + let observer = self.observer.clone(); + let handler_registry = self.handler_registry.clone(); + + thread::spawn(move || loop { + let event_opt = { + let mut observer_lock = observer.lock().unwrap(); + observer_lock.iter_events().next() + }; + + if let Some(event) = event_opt { + handler_registry.lock().unwrap().handle_event(&event); + } + }); + } +} diff --git a/kclvm/tools/src/LSP/src/lib.rs b/kclvm/tools/src/LSP/src/lib.rs index a67f1f53a..aea3afe3a 100644 --- a/kclvm/tools/src/LSP/src/lib.rs +++ b/kclvm/tools/src/LSP/src/lib.rs @@ -2,15 +2,18 @@ mod analysis; mod capabilities; mod completion; mod config; +mod config_manager; mod db; mod dispatcher; mod document_symbol; mod error; +mod file; mod find_refs; mod formatting; mod from_lsp; mod goto_def; mod hover; +mod kcl_watch_system; mod main_loop; mod notification; mod quick_fix; diff --git a/kclvm/tools/src/LSP/src/main.rs b/kclvm/tools/src/LSP/src/main.rs index 9a8e8aeeb..445d9b969 100644 --- a/kclvm/tools/src/LSP/src/main.rs +++ b/kclvm/tools/src/LSP/src/main.rs @@ -1,11 +1,13 @@ use crate::main_loop::main_loop; use config::Config; +use kcl_watch_system::KCLWatchSystem; use main_loop::app; mod analysis; mod capabilities; mod completion; mod config; +mod config_manager; mod db; mod dispatcher; mod document_symbol; @@ -28,6 +30,9 @@ mod formatting; #[cfg(test)] mod tests; +mod file; +mod kcl_watch_system; // Import the new module + /// Main entry point for the `kcl-language-server` executable. fn main() -> Result<(), anyhow::Error> { let args: Vec = std::env::args().collect(); @@ -82,8 +87,21 @@ fn run_server() -> anyhow::Result<()> { .map_err(|_| anyhow::anyhow!("Initialize result error"))?; connection.initialize_finish(initialize_id, initialize_result)?; - let config = Config::default(); + + // Load configuration from file + let config_file = + config_manager::config::get_config_file().expect("Failed to find configuration file"); + let config = + config_manager::Config::load_from_file(&config_file).expect("Failed to load configuration"); + + // Create a new KCL Watch System instance + let kcl_watch_system = KCLWatchSystem::new(config.clone()); + + // Start the observer + kcl_watch_system.start_observer(); + main_loop(connection, config, initialize_params)?; + io_threads.join()?; Ok(()) } diff --git a/kclvm/tools/src/LSP/src/notification.rs b/kclvm/tools/src/LSP/src/notification.rs index ac14cc72d..3734cfbea 100644 --- a/kclvm/tools/src/LSP/src/notification.rs +++ b/kclvm/tools/src/LSP/src/notification.rs @@ -1,10 +1,3 @@ -use kclvm_config::{modfile::KCL_MOD_FILE, settings::DEFAULT_SETTING_FILE}; -use lsp_types::notification::{ - Cancel, DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, - DidOpenTextDocument, DidSaveTextDocument, -}; -use std::path::Path; - use crate::{ dispatcher::NotificationDispatcher, from_lsp, @@ -12,6 +5,12 @@ use crate::{ util::apply_document_changes, word_index::{build_word_index_with_content, word_index_add, word_index_subtract}, }; +use kclvm_config::{modfile::KCL_MOD_FILE, settings::DEFAULT_SETTING_FILE}; +use lsp_types::notification::{ + Cancel, DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, + DidOpenTextDocument, DidSaveTextDocument, +}; +use std::path::Path; impl LanguageServerState { pub fn on_notification( @@ -59,7 +58,7 @@ impl LanguageServerState { Ok(()) } - /// Called when a `DidChangeTextDocument` notification was received. + /// Called when a `DidSaveTextDocument` notification was received. fn on_did_save_text_document( &mut self, params: lsp_types::DidSaveTextDocumentParams, diff --git a/kclvm/tools/src/LSP/src/observer.json b/kclvm/tools/src/LSP/src/observer.json new file mode 100644 index 000000000..b20f26ab4 --- /dev/null +++ b/kclvm/tools/src/LSP/src/observer.json @@ -0,0 +1,5 @@ +{ + "path": "./", + "recursive": true, + "patterns": ["k", "mod", "json", "yaml"] +}