From e73f31232d31f5a09a9e1991c5ac27cdf6ac36c5 Mon Sep 17 00:00:00 2001 From: he1pa <18012015693@163.com> Date: Wed, 6 Nov 2024 18:30:23 +0800 Subject: [PATCH 1/5] feat: lsp file watcher. Actively monitor file system changes. These changes will not be notified through lsp, e.g., execute `kcl mod add xxx`, `kcl fmt xxx` Signed-off-by: he1pa <18012015693@163.com> --- kclvm/Cargo.lock | 37 ++++- kclvm/driver/Cargo.toml | 1 - kclvm/sema/src/resolver/import.rs | 1 + kclvm/sema/src/resolver/mod.rs | 10 +- kclvm/tools/src/LSP/Cargo.toml | 3 +- kclvm/tools/src/LSP/src/notification.rs | 26 +-- kclvm/tools/src/LSP/src/request.rs | 20 ++- kclvm/tools/src/LSP/src/state.rs | 200 +++++++++++++++++++++--- kclvm/tools/src/LSP/src/tests.rs | 88 +++++++++++ kclvm/tools/src/LSP/src/util.rs | 14 +- 10 files changed, 335 insertions(+), 65 deletions(-) diff --git a/kclvm/Cargo.lock b/kclvm/Cargo.lock index 73cf06e87..53d2192ed 100644 --- a/kclvm/Cargo.lock +++ b/kclvm/Cargo.lock @@ -1535,6 +1535,17 @@ dependencies = [ "libc", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + [[package]] name = "inotify-sys" version = "0.1.5" @@ -1734,6 +1745,7 @@ dependencies = [ "lsp-server", "lsp-types", "maplit", + "notify 7.0.0", "parking_lot 0.12.3", "proc_macro_crate", "ra_ap_vfs", @@ -1914,7 +1926,6 @@ dependencies = [ "kclvm-parser", "kclvm-runtime", "kclvm-utils", - "notify 6.1.1", "oci-distribution", "once_cell", "parking_lot 0.12.3", @@ -2453,6 +2464,7 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -2482,7 +2494,7 @@ dependencies = [ "crossbeam-channel", "filetime", "fsevent-sys", - "inotify", + "inotify 0.9.6", "kqueue", "libc", "mio 0.8.11", @@ -2492,21 +2504,30 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ "bitflags 2.6.0", - "crossbeam-channel", "filetime", "fsevent-sys", - "inotify", + "inotify 0.10.2", "kqueue", "libc", "log", - "mio 0.8.11", + "mio 1.0.1", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df" +dependencies = [ + "instant", ] [[package]] diff --git a/kclvm/driver/Cargo.toml b/kclvm/driver/Cargo.toml index d7f81e37d..05d15f66b 100644 --- a/kclvm/driver/Cargo.toml +++ b/kclvm/driver/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] serde_json = "1.0.86" -notify = "6.1.1" kclvm-config ={ path = "../config"} kclvm-runtime ={ path = "../runtime"} diff --git a/kclvm/sema/src/resolver/import.rs b/kclvm/sema/src/resolver/import.rs index 48718289c..c2a9a6a25 100644 --- a/kclvm/sema/src/resolver/import.rs +++ b/kclvm/sema/src/resolver/import.rs @@ -42,6 +42,7 @@ impl<'ctx> Resolver<'ctx> { let real_path = Path::new(&self.program.root).join(pkgpath.replace('.', "/")); if !self.program.pkgs.contains_key(pkgpath) { + self.ctx.invalid_pkg_scope.insert(pkgpath.to_string()); if real_path.exists() { self.handler.add_error( ErrorKind::CannotFindModule, diff --git a/kclvm/sema/src/resolver/mod.rs b/kclvm/sema/src/resolver/mod.rs index 5ec94c590..6834d7137 100644 --- a/kclvm/sema/src/resolver/mod.rs +++ b/kclvm/sema/src/resolver/mod.rs @@ -19,7 +19,7 @@ mod var; #[cfg(test)] mod tests; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use kclvm_error::diagnostic::Range; use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; @@ -99,8 +99,12 @@ impl<'ctx> Resolver<'ctx> { pub(crate) fn check_and_lint(&mut self, pkgpath: &str) -> ProgramScope { self.check(pkgpath); + let mut scope_map = self.scope_map.clone(); + for invalid_pkg_scope in &self.ctx.invalid_pkg_scope { + scope_map.remove(invalid_pkg_scope); + } let mut scope = ProgramScope { - scope_map: self.scope_map.clone(), + scope_map, import_names: self.ctx.import_names.clone(), node_ty_map: self.node_ty_map.clone(), handler: self.handler.clone(), @@ -145,6 +149,8 @@ pub struct Context { pub ty_ctx: TypeContext, /// Type alias mapping pub type_alias_mapping: IndexMap>, + /// invalid pkg scope, remove when after resolve + pub invalid_pkg_scope: IndexSet, } /// Resolve options. diff --git a/kclvm/tools/src/LSP/Cargo.toml b/kclvm/tools/src/LSP/Cargo.toml index 1cfa4e722..7ee36ea5c 100644 --- a/kclvm/tools/src/LSP/Cargo.toml +++ b/kclvm/tools/src/LSP/Cargo.toml @@ -37,13 +37,14 @@ anyhow = { version = "1.0", default-features = false, features = ["std"] } crossbeam-channel = { version = "0.5.7", default-features = false } ra_ap_vfs = "0.0.149" ra_ap_vfs-notify = "0.0.149" -lsp-types = { version = "0.93.0", features = ["proposed"]} +lsp-types = { version = "0.93.0", features = ["proposed"] } threadpool = { version = "1.8.1", default-features = false } salsa = { version = "0.16.1", default-features = false } serde_json = { version = "1.0", default-features = false } parking_lot = { version = "0.12.0", default-features = false } rustc-hash = { version = "1.1.0", default-features = false } proc_macro_crate = { path = "../../benches/proc_macro_crate" } +notify = "7.0.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.37.0", features = ["full"] } diff --git a/kclvm/tools/src/LSP/src/notification.rs b/kclvm/tools/src/LSP/src/notification.rs index 3e1bae1a1..f73b7488a 100644 --- a/kclvm/tools/src/LSP/src/notification.rs +++ b/kclvm/tools/src/LSP/src/notification.rs @@ -1,13 +1,8 @@ -use kclvm_config::{ - modfile::{KCL_MOD_FILE, KCL_WORK_FILE}, - settings::DEFAULT_SETTING_FILE, -}; -use kclvm_driver::lookup_compile_workspaces; use lsp_types::notification::{ Cancel, DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument, }; -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; use crate::util::apply_document_changes; use crate::{ @@ -137,27 +132,8 @@ impl LanguageServerState { for change in params.changes { let path = from_lsp::abs_path(&change.uri)?; self.loader.handle.invalidate(path.clone()); - if KCL_CONFIG_FILE.contains(&path.file_name().unwrap().to_str().unwrap()) { - self.entry_cache.write().clear(); - let parent_path = path.parent().unwrap(); - let path = parent_path.as_os_str().to_str().unwrap().to_string(); - let tool = Arc::clone(&self.tool); - let (workspaces, failed) = lookup_compile_workspaces(&*tool.read(), &path, true); - - if let Some(failed) = failed { - for (key, err) in failed { - self.log_message(format!("parse kcl.work failed: {}: {}", key, err)); - } - } - - for (workspace, opts) in workspaces { - self.async_compile(workspace, opts, None, false); - } - } } Ok(()) } } - -const KCL_CONFIG_FILE: [&str; 3] = [DEFAULT_SETTING_FILE, KCL_MOD_FILE, KCL_WORK_FILE]; diff --git a/kclvm/tools/src/LSP/src/request.rs b/kclvm/tools/src/LSP/src/request.rs index 0f862d94e..a854ecc46 100644 --- a/kclvm/tools/src/LSP/src/request.rs +++ b/kclvm/tools/src/LSP/src/request.rs @@ -1,6 +1,7 @@ use anyhow::anyhow; use crossbeam_channel::Sender; +use kclvm_driver::WorkSpaceKind; use kclvm_sema::info::is_valid_kcl_name; use lsp_types::{Location, SemanticTokensResult, TextEdit}; use ra_ap_vfs::VfsPath; @@ -97,7 +98,7 @@ impl LanguageServerSnapshot { ) -> anyhow::Result>> { match self.try_get_db_state(path) { Ok(db) => match db { - Some(db) => match db { + Some((_, db)) => match db { DBState::Ready(db) => Ok(Some(db.clone())), DBState::Compiling(_) | DBState::Init => { log_message( @@ -124,7 +125,10 @@ impl LanguageServerSnapshot { /// return Ok(Some(db)) -> Compile completed /// return Ok(None) -> RWlock, retry to unlock /// return Err(_) -> Compile failed - pub(crate) fn try_get_db_state(&self, path: &VfsPath) -> anyhow::Result> { + pub(crate) fn try_get_db_state( + &self, + path: &VfsPath, + ) -> anyhow::Result> { match self.vfs.try_read() { Some(vfs) => match vfs.file_id(path) { Some(file_id) => { @@ -134,7 +138,7 @@ impl LanguageServerSnapshot { Some(option_workspace) => match option_workspace { Some(work_space) => match self.workspaces.try_read() { Some(workspaces) => match workspaces.get(work_space) { - Some(db) => Ok(Some(db.clone())), + Some(db) => Ok(Some((work_space.clone(), db.clone()))), None => Err(anyhow::anyhow!( LSPError::AnalysisDatabaseNotFound(path.clone()) )), @@ -154,7 +158,7 @@ impl LanguageServerSnapshot { let work_space = file_info.workspaces.iter().next().unwrap(); match self.workspaces.try_read() { Some(workspaces) => match workspaces.get(work_space) { - Some(db) => Ok(Some(db.clone())), + Some(db) => Ok(Some((work_space.clone(), db.clone()))), None => Err(anyhow::anyhow!( LSPError::AnalysisDatabaseNotFound(path.clone()) )), @@ -310,7 +314,7 @@ pub(crate) fn handle_completion( return Ok(None); } - let db_state = match snapshot.try_get_db_state(&path) { + let (workspace, db_state) = match snapshot.try_get_db_state(&path) { Ok(option_state) => match option_state { Some(db) => db, None => return Err(anyhow!(LSPError::Retry)), @@ -341,10 +345,10 @@ pub(crate) fn handle_completion( let kcl_pos = kcl_pos(&file, params.text_document_position.position); let metadata = snapshot - .entry_cache + .workspace_config_cache .read() - .get(&file) - .and_then(|metadata| metadata.0 .2.clone()); + .get(&workspace) + .and_then(|opt| opt.2.clone()); let res = completion( completion_trigger_character, diff --git a/kclvm/tools/src/LSP/src/state.rs b/kclvm/tools/src/LSP/src/state.rs index edc5f8360..e3d24be1a 100644 --- a/kclvm/tools/src/LSP/src/state.rs +++ b/kclvm/tools/src/LSP/src/state.rs @@ -2,11 +2,13 @@ use crate::analysis::{Analysis, AnalysisDatabase, DBState, OpenFileInfo}; use crate::compile::{compile, Params}; use crate::from_lsp::file_path_from_url; use crate::to_lsp::{kcl_diag_to_lsp_diags, url_from_path}; -use crate::util::{get_file_name, to_json}; +use crate::util::{filter_kcl_config_file, get_file_name, to_json}; use crossbeam_channel::{select, unbounded, Receiver, Sender}; use indexmap::IndexSet; use kclvm_driver::toolchain::{self, Toolchain}; -use kclvm_driver::{lookup_compile_workspaces, CompileUnitOptions, WorkSpaceKind}; +use kclvm_driver::{ + lookup_compile_workspace, lookup_compile_workspaces, CompileUnitOptions, WorkSpaceKind, +}; use kclvm_parser::KCLModuleCache; use kclvm_sema::core::global_state::GlobalState; use kclvm_sema::resolver::scope::KCLScopeCache; @@ -16,13 +18,15 @@ use lsp_types::{ notification::{Notification, PublishDiagnostics}, InitializeParams, PublishDiagnosticsParams, WorkspaceFolder, }; +use notify::{FsEventWatcher, RecursiveMode, Watcher}; use parking_lot::RwLock; use ra_ap_vfs::{ChangeKind, ChangedFile, FileId, Vfs}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::thread; -use std::time::{Duration, SystemTime}; -use std::{sync::Arc, time::Instant}; +use std::time::Duration; +use std::{sync::mpsc, sync::Arc, time::Instant}; pub(crate) type RequestHandler = fn(&mut LanguageServerState, lsp_server::Response); @@ -41,6 +45,15 @@ pub(crate) enum Task { pub(crate) enum Event { Task(Task), Lsp(lsp_server::Message), + FileWatcher(FileWatcherEvent), +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) enum FileWatcherEvent { + ChangedConfigFile(Vec), + RemoveConfigFile(Vec), + CreateConfigFile(Vec), } pub(crate) struct Handle { @@ -49,8 +62,6 @@ pub(crate) struct Handle { } pub(crate) type KCLVfs = Arc>; -pub(crate) type KCLEntryCache = - Arc)>>>; pub(crate) type KCLWorkSpaceConfigCache = Arc>>; @@ -85,17 +96,21 @@ pub(crate) struct LanguageServerState { pub module_cache: KCLModuleCache, /// KCL resolver cache pub scope_cache: KCLScopeCache, - /// KCL compile unit cache cache - pub entry_cache: KCLEntryCache, /// Toolchain is used to provider KCL tool features for the language server. pub tool: KCLToolChain, /// KCL globalstate cache pub gs_cache: KCLGlobalStateCache, - + /// Compile config cache pub workspace_config_cache: KCLWorkSpaceConfigCache, /// Process files that are not in any defined workspace and delete the workspace when closing the file pub temporary_workspace: Arc>>>, pub workspace_folders: Option>, + /// Actively monitor file system changes. These changes will not be notified through lsp, + /// e.g., execute `kcl mod add xxx`, `kcl fmt xxx` + pub fs_event_watcher: Handle< + Box, + mpsc::Receiver>, + >, } /// A snapshot of the state of the language server @@ -113,12 +128,12 @@ pub(crate) struct LanguageServerSnapshot { pub module_cache: KCLModuleCache, /// KCL resolver cache pub scope_cache: KCLScopeCache, - /// KCL compile unit cache cache - pub entry_cache: KCLEntryCache, /// Toolchain is used to provider KCL tool features for the language server. pub tool: KCLToolChain, /// Process files that are not in any defined workspace and delete the work pub temporary_workspace: Arc>>>, + /// Compile config cache + pub workspace_config_cache: KCLWorkSpaceConfigCache, } #[allow(unused)] @@ -134,6 +149,16 @@ impl LanguageServerState { Handle { handle, _receiver } }; + let fs_event_watcher = { + let (tx, rx) = mpsc::channel::>(); + let mut watcher = notify::recommended_watcher(tx).unwrap(); + let handle = Box::new(watcher); + Handle { + handle, + _receiver: rx, + } + }; + let mut state = LanguageServerState { sender, request_queue: ReqQueue::default(), @@ -147,13 +172,13 @@ impl LanguageServerState { loader, module_cache: KCLModuleCache::default(), scope_cache: KCLScopeCache::default(), - entry_cache: KCLEntryCache::default(), tool: Arc::new(RwLock::new(toolchain::default())), gs_cache: KCLGlobalStateCache::default(), request_retry: Arc::new(RwLock::new(HashMap::new())), workspace_config_cache: KCLWorkSpaceConfigCache::default(), temporary_workspace: Arc::new(RwLock::new(HashMap::new())), workspace_folders: initialize_params.workspace_folders.clone(), + fs_event_watcher, }; state.init_workspaces(); @@ -164,9 +189,53 @@ impl LanguageServerState { /// Blocks until a new event is received from one of the many channels the language server /// listens to. Returns the first event that is received. fn next_event(&self, receiver: &Receiver) -> Option { + for event in self.fs_event_watcher._receiver.try_iter() { + if let Ok(e) = event { + match e.kind { + notify::EventKind::Modify(modify_kind) => { + if let notify::event::ModifyKind::Data(data_change) = modify_kind { + if let notify::event::DataChange::Content = data_change { + let paths = e.paths; + let kcl_config_file: Vec = filter_kcl_config_file(&paths); + if !kcl_config_file.is_empty() { + return Some(Event::FileWatcher( + FileWatcherEvent::ChangedConfigFile(kcl_config_file), + )); + } + } + } + } + notify::EventKind::Remove(remove_kind) => { + if let notify::event::RemoveKind::File = remove_kind { + let paths = e.paths; + let kcl_config_file: Vec = filter_kcl_config_file(&paths); + if !kcl_config_file.is_empty() { + return Some(Event::FileWatcher( + FileWatcherEvent::RemoveConfigFile(kcl_config_file), + )); + } + } + } + + notify::EventKind::Create(create_kind) => { + if let notify::event::CreateKind::File = create_kind { + let paths = e.paths; + let kcl_config_file: Vec = filter_kcl_config_file(&paths); + if !kcl_config_file.is_empty() { + return Some(Event::FileWatcher( + FileWatcherEvent::CreateConfigFile(kcl_config_file), + )); + } + } + } + _ => {} + } + } + } + select! { recv(receiver) -> msg => msg.ok().map(Event::Lsp), - recv(self.task_receiver) -> task => Some(Event::Task(task.unwrap())) + recv(self.task_receiver) -> task => Some(Event::Task(task.unwrap())), } } @@ -197,6 +266,9 @@ impl LanguageServerState { _ => {} } } + Event::FileWatcher(file_watcher_event) => { + self.handle_file_watcher_event(file_watcher_event)? + } }; // 2. Process changes @@ -268,9 +340,23 @@ impl LanguageServerState { "Not contains in any workspace, compile: {:?}", filename )); + let tool = Arc::clone(&self.tool); - let (workspaces, failed) = - lookup_compile_workspaces(&*tool.read(), &filename, true); + let (workspaces, failed) = match Path::new(&filename).parent() { + Some(parent_dir) => { + let (workspaces, failed) = lookup_compile_workspaces( + &*tool.read(), + &parent_dir.to_str().unwrap().to_string(), + true, + ); + if workspaces.is_empty() { + lookup_compile_workspaces(&*tool.read(), &filename, true) + } else { + (workspaces, failed) + } + } + None => lookup_compile_workspaces(&*tool.read(), &filename, true), + }; if workspaces.is_empty() { self.temporary_workspace.write().remove(&file.file_id); @@ -397,6 +483,17 @@ impl LanguageServerState { Ok(()) } + /// Handles a task sent by another async task + #[allow(clippy::unnecessary_wraps)] + fn handle_file_watcher_event(&mut self, event: FileWatcherEvent) -> anyhow::Result<()> { + match event { + FileWatcherEvent::ChangedConfigFile(paths) => self.handle_changed_confg_file(&paths), + FileWatcherEvent::CreateConfigFile(paths) => self.handle_create_confg_file(&paths), + FileWatcherEvent::RemoveConfigFile(paths) => self.handle_remove_confg_file(&paths), + } + Ok(()) + } + /// Sends a response to the client. This method logs the time it took us to reply /// to a request from the client. pub(super) fn respond(&mut self, response: lsp_server::Response) -> anyhow::Result<()> { @@ -438,11 +535,11 @@ impl LanguageServerState { opened_files: self.opened_files.clone(), module_cache: self.module_cache.clone(), scope_cache: self.scope_cache.clone(), - entry_cache: self.entry_cache.clone(), tool: self.tool.clone(), request_retry: self.request_retry.clone(), workspaces: self.analysis.workspaces.clone(), temporary_workspace: self.temporary_workspace.clone(), + workspace_config_cache: self.workspace_config_cache.clone(), } } @@ -464,6 +561,11 @@ impl LanguageServerState { if let Some(workspace_folders) = &self.workspace_folders { for folder in workspace_folders { let path = file_path_from_url(&folder.uri).unwrap(); + let mut watcher = &mut self.fs_event_watcher.handle; + watcher + .watch(std::path::Path::new(&path), RecursiveMode::Recursive) + .unwrap(); + self.log_message(format!("Start watch {:?}", path)); let tool = Arc::clone(&self.tool); let (workspaces, failed) = lookup_compile_workspaces(&*tool.read(), &path, true); @@ -501,7 +603,6 @@ impl LanguageServerState { let sender = self.task_sender.clone(); let module_cache = Arc::clone(&self.module_cache); let scope_cache = Arc::clone(&self.scope_cache); - let entry = Arc::clone(&self.entry_cache); let tool = Arc::clone(&self.tool); let gs_cache = Arc::clone(&self.gs_cache); @@ -541,15 +642,17 @@ impl LanguageServerState { gs_cache: Some(gs_cache), }, &mut files, - opts.1, + opts.1.clone(), ); log_message( format!( - "Compile workspace: {:?}, main_pkg files: {:?}, changed file: {:?}, use {:?} micros", + "Compile workspace: {:?}, main_pkg files: {:?}, changed file: {:?}, options: {:?}, metadate: {:?}, use {:?} micros", workspace, files, filename, + opts.1, + opts.2, start.elapsed().as_micros() ), &sender, @@ -656,6 +759,65 @@ impl LanguageServerState { } }) } + + // Configuration file modifications that do not occur on the IDE client side, e.g., `kcl mod add xxx`` + pub(crate) fn handle_changed_confg_file(&self, paths: &[PathBuf]) { + for path in paths { + self.log_message(format!("Changed config file {:?}", path)); + // In workspaces + let mut workspaces = self.analysis.workspaces.write(); + for workspace in workspaces.keys() { + if let Some(p) = match workspace { + WorkSpaceKind::ModFile(path_buf) => Some(path_buf.clone()), + WorkSpaceKind::SettingFile(path_buf) => Some(path_buf.clone()), + _ => None, + } { + let opts = + lookup_compile_workspace(&*self.tool.read(), &p.to_str().unwrap(), true); + self.async_compile(workspace.clone(), opts, None, false); + } + } + drop(workspaces); + + // In temp workspaces + let mut temp_workspace = self.temporary_workspace.write(); + + for (file_id, workspace) in temp_workspace.iter_mut() { + if let Some(p) = if let Some(w) = workspace { + match w { + WorkSpaceKind::ModFile(path_buf) => Some(path_buf.clone()), + WorkSpaceKind::SettingFile(path_buf) => Some(path_buf.clone()), + _ => None, + } + } else { + None + } { + let opts = + lookup_compile_workspace(&*self.tool.read(), &p.to_str().unwrap(), true); + self.async_compile( + workspace.clone().unwrap(), + opts, + Some(file_id.clone()), + false, + ); + } + } + } + } + + fn handle_create_confg_file(&self, paths: &[PathBuf]) { + for path in paths { + // Just log, nothing to do + self.log_message(format!("Create config file: {:?}", path)); + } + } + + fn handle_remove_confg_file(&self, paths: &[PathBuf]) { + for path in paths { + self.log_message(format!("Remove config file: {:?}", path)); + // todo: clear cache + } + } } pub(crate) fn log_message(message: String, sender: &Sender) -> anyhow::Result<()> { diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index f34ee5d6a..a4e7d4f27 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -1411,6 +1411,94 @@ fn formatting_unsaved_test() { ) } +#[test] +fn complete_import_external_file_e2e_test() { + let path = PathBuf::from(".") + .join("src") + .join("test_data") + .join("completion_test") + .join("import") + .join("external") + .join("external_1") + .join("main.k") + .canonicalize() + .unwrap() + .display() + .to_string(); + + let _ = Command::new("kcl") + .arg("mod") + .arg("metadata") + .arg("--update") + .current_dir( + PathBuf::from(".") + .join("src") + .join("test_data") + .join("completion_test") + .join("import") + .join("external") + .join("external_1") + .canonicalize() + .unwrap() + .display() + .to_string(), + ) + .output() + .unwrap(); + let src = std::fs::read_to_string(path.clone()).unwrap(); + let server = Project {}.server(InitializeParams::default()); + + // Mock open file + server.notification::( + lsp_types::DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: Url::from_file_path(path.clone()).unwrap(), + language_id: "KCL".to_string(), + version: 0, + text: src, + }, + }, + ); + + let id = server.next_request_id.get(); + server.next_request_id.set(id.wrapping_add(1)); + + let r: Request = Request::new( + id.into(), + "textDocument/completion".to_string(), + CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: Url::from_file_path(path).unwrap(), + }, + position: Position::new(0, 7), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + context: None, + }, + ); + + // Send request and wait for it's response + let res = server.send_and_receive(r); + match res.result.unwrap() { + serde_json::Value::Array(vec) => { + assert!( + (vec.iter() + .find(|v| match v { + serde_json::Value::Object(map) => { + map.get("label").unwrap() == "k8s" + } + _ => false, + }) + .is_some()), + "" + ); + } + _ => panic!("test failed"), + } +} + // Integration testing of lsp and konfig fn konfig_path() -> PathBuf { let konfig_path = Path::new(".") diff --git a/kclvm/tools/src/LSP/src/util.rs b/kclvm/tools/src/LSP/src/util.rs index 9ad208656..42d092a1a 100644 --- a/kclvm/tools/src/LSP/src/util.rs +++ b/kclvm/tools/src/LSP/src/util.rs @@ -16,7 +16,7 @@ use ra_ap_vfs::{FileId, Vfs}; use serde::{de::DeserializeOwned, Serialize}; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; /// Deserializes a `T` from a json value. pub(crate) fn from_json( @@ -107,6 +107,18 @@ pub(crate) fn load_files_code_from_vfs( Ok(res) } +pub(crate) fn filter_kcl_config_file(paths: &[PathBuf]) -> Vec { + paths + .iter() + .filter(|p| { + p.file_name().map(|n| n.to_str().unwrap()) == Some(kclvm_config::modfile::KCL_MOD_FILE) + || p.file_name().map(|n| n.to_str().unwrap()) + == Some(kclvm_config::settings::DEFAULT_SETTING_FILE) + }) + .map(|p| p.clone()) + .collect() +} + macro_rules! walk_if_contains { ($expr: expr, $pos: expr, $schema_def: expr) => { if $expr.contains_pos($pos) { From fbed221b0dc568e914c93495835592eb27027764 Mon Sep 17 00:00:00 2001 From: he1pa <18012015693@163.com> Date: Wed, 6 Nov 2024 19:08:58 +0800 Subject: [PATCH 2/5] s/FsEventWatcher/RecommendedWatcher Signed-off-by: he1pa <18012015693@163.com> --- kclvm/tools/src/LSP/src/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kclvm/tools/src/LSP/src/state.rs b/kclvm/tools/src/LSP/src/state.rs index e3d24be1a..8c1d05b7b 100644 --- a/kclvm/tools/src/LSP/src/state.rs +++ b/kclvm/tools/src/LSP/src/state.rs @@ -18,7 +18,7 @@ use lsp_types::{ notification::{Notification, PublishDiagnostics}, InitializeParams, PublishDiagnosticsParams, WorkspaceFolder, }; -use notify::{FsEventWatcher, RecursiveMode, Watcher}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use parking_lot::RwLock; use ra_ap_vfs::{ChangeKind, ChangedFile, FileId, Vfs}; use std::collections::HashMap; @@ -108,7 +108,7 @@ pub(crate) struct LanguageServerState { /// Actively monitor file system changes. These changes will not be notified through lsp, /// e.g., execute `kcl mod add xxx`, `kcl fmt xxx` pub fs_event_watcher: Handle< - Box, + Box, mpsc::Receiver>, >, } From 344905d52456ff45fc2c8c64d0397a7bf9edc78b Mon Sep 17 00:00:00 2001 From: he1pa <18012015693@163.com> Date: Thu, 7 Nov 2024 11:23:14 +0800 Subject: [PATCH 3/5] add lsp watcher ut Signed-off-by: he1pa <18012015693@163.com> --- kclvm/tools/src/LSP/src/state.rs | 1 - .../src/LSP/src/test_data/compile_unit/b.k | 1 - .../LSP/src/test_data/compile_unit/kcl.yaml | 4 - .../src/LSP/src/test_data/compile_unit/main.k | 3 - .../LSP/src/test_data/watcher/modify/kcl.mod | 6 + .../a.k => watcher/modify/main.k} | 0 kclvm/tools/src/LSP/src/tests.rs | 111 +++++++++++++++++- 7 files changed, 112 insertions(+), 14 deletions(-) delete mode 100644 kclvm/tools/src/LSP/src/test_data/compile_unit/b.k delete mode 100644 kclvm/tools/src/LSP/src/test_data/compile_unit/kcl.yaml delete mode 100644 kclvm/tools/src/LSP/src/test_data/compile_unit/main.k create mode 100644 kclvm/tools/src/LSP/src/test_data/watcher/modify/kcl.mod rename kclvm/tools/src/LSP/src/test_data/{compile_unit/a.k => watcher/modify/main.k} (100%) diff --git a/kclvm/tools/src/LSP/src/state.rs b/kclvm/tools/src/LSP/src/state.rs index 8c1d05b7b..c31e4b950 100644 --- a/kclvm/tools/src/LSP/src/state.rs +++ b/kclvm/tools/src/LSP/src/state.rs @@ -644,7 +644,6 @@ impl LanguageServerState { &mut files, opts.1.clone(), ); - log_message( format!( "Compile workspace: {:?}, main_pkg files: {:?}, changed file: {:?}, options: {:?}, metadate: {:?}, use {:?} micros", diff --git a/kclvm/tools/src/LSP/src/test_data/compile_unit/b.k b/kclvm/tools/src/LSP/src/test_data/compile_unit/b.k deleted file mode 100644 index d25d49e0f..000000000 --- a/kclvm/tools/src/LSP/src/test_data/compile_unit/b.k +++ /dev/null @@ -1 +0,0 @@ -a = 1 \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/test_data/compile_unit/kcl.yaml b/kclvm/tools/src/LSP/src/test_data/compile_unit/kcl.yaml deleted file mode 100644 index 12f641005..000000000 --- a/kclvm/tools/src/LSP/src/test_data/compile_unit/kcl.yaml +++ /dev/null @@ -1,4 +0,0 @@ -kcl_cli_configs: - files: - - main.k - - a.k \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/test_data/compile_unit/main.k b/kclvm/tools/src/LSP/src/test_data/compile_unit/main.k deleted file mode 100644 index 93d88b96d..000000000 --- a/kclvm/tools/src/LSP/src/test_data/compile_unit/main.k +++ /dev/null @@ -1,3 +0,0 @@ -import .b - -_b = b.a diff --git a/kclvm/tools/src/LSP/src/test_data/watcher/modify/kcl.mod b/kclvm/tools/src/LSP/src/test_data/watcher/modify/kcl.mod new file mode 100644 index 000000000..3b4edd7c7 --- /dev/null +++ b/kclvm/tools/src/LSP/src/test_data/watcher/modify/kcl.mod @@ -0,0 +1,6 @@ +[package] +name = "add" +edition = "v0.9.0" +version = "0.0.1" + +[dependencies] diff --git a/kclvm/tools/src/LSP/src/test_data/compile_unit/a.k b/kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k similarity index 100% rename from kclvm/tools/src/LSP/src/test_data/compile_unit/a.k rename to kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index a4e7d4f27..fe7591edd 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -88,10 +88,13 @@ use crate::to_lsp::kcl_diag_to_lsp_diags_by_file; use crate::util::apply_document_changes; use crate::util::to_json; -macro_rules! wait_async_compile { +macro_rules! wait_async { () => { thread::sleep(Duration::from_millis(100)); }; + ($time_ms:expr) => { + thread::sleep(Duration::from_millis($time_ms)); + }; } pub(crate) fn compare_goto_res( @@ -662,7 +665,7 @@ impl Server { /// Sends a server notification to the main loop fn send_notification(&self, not: Notification) { self.client.sender.send(Message::Notification(not)).unwrap(); - wait_async_compile!(); + wait_async!(); } /// A function to wait for a specific message to arrive @@ -1499,6 +1502,104 @@ fn complete_import_external_file_e2e_test() { } } +#[test] +fn mod_file_watcher_test() { + let path = PathBuf::from(".") + .join("src") + .join("test_data") + .join("watcher") + .join("modify") + .canonicalize() + .unwrap(); + + let mod_file_path = path.join("kcl.mod"); + let main_path = path.join("main.k"); + + let mod_src_bac = std::fs::read_to_string(mod_file_path.clone()).unwrap(); + let main_src = std::fs::read_to_string(main_path.clone()).unwrap(); + + let initialize_params = InitializeParams { + workspace_folders: Some(vec![WorkspaceFolder { + uri: Url::from_file_path(path.clone()).unwrap(), + name: "test".to_string(), + }]), + ..Default::default() + }; + let server = Project {}.server(initialize_params); + + // Mock open file + server.notification::( + lsp_types::DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: Url::from_file_path(main_path.clone()).unwrap(), + language_id: "KCL".to_string(), + version: 0, + text: main_src, + }, + }, + ); + + let _ = Command::new("kcl") + .arg("mod") + .arg("add") + .arg("helloworld") + .current_dir(path) + .output() + .unwrap(); + + // wait for download dependice + wait_async!(500); + + server.notification::( + lsp_types::DidChangeTextDocumentParams { + text_document: lsp_types::VersionedTextDocumentIdentifier { + uri: Url::from_file_path(main_path.clone()).unwrap(), + version: 1, + }, + content_changes: vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "import helloworld".to_string(), + }], + }, + ); + + let id = server.next_request_id.get(); + server.next_request_id.set(id.wrapping_add(1)); + + let r: Request = Request::new( + id.into(), + "textDocument/hover".to_string(), + HoverParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: Url::from_file_path(main_path).unwrap(), + }, + position: Position::new(0, 8), + }, + work_done_progress_params: Default::default(), + }, + ); + + // Send request and wait for it's response + let res = server.send_and_receive(r); + + std::fs::write(mod_file_path, mod_src_bac).unwrap(); + assert_eq!( + res.result.unwrap(), + to_json(Hover { + contents: HoverContents::Scalar(MarkedString::LanguageString( + lsp_types::LanguageString { + language: "KCL".to_owned(), + value: "helloworld: ".to_string(), + } + )), + range: None + }) + .unwrap() + ) +} + // Integration testing of lsp and konfig fn konfig_path() -> PathBuf { let konfig_path = Path::new(".") @@ -2010,7 +2111,7 @@ fn find_refs_test() { let server = Project {}.server(initialize_params); // Wait for async build word_index_map - wait_async_compile!(); + wait_async!(); let url = Url::from_file_path(path).unwrap(); @@ -2105,7 +2206,7 @@ fn find_refs_with_file_change_test() { let server = Project {}.server(initialize_params); // Wait for async build word_index_map - wait_async_compile!(); + wait_async!(); let url = Url::from_file_path(path).unwrap(); @@ -2214,7 +2315,7 @@ fn rename_test() { }; let server = Project {}.server(initialize_params); - wait_async_compile!(); + wait_async!(); let url = Url::from_file_path(path).unwrap(); let main_url = Url::from_file_path(main_path).unwrap(); From e2f0ab38ad00f494f5958f4636af0e3a3bbbf1e2 Mon Sep 17 00:00:00 2001 From: he1pa <18012015693@163.com> Date: Thu, 7 Nov 2024 11:30:26 +0800 Subject: [PATCH 4/5] fix typo Signed-off-by: he1pa <18012015693@163.com> --- kclvm/tools/src/LSP/src/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index fe7591edd..6dac71010 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -1547,7 +1547,7 @@ fn mod_file_watcher_test() { .output() .unwrap(); - // wait for download dependice + // wait for download dependence wait_async!(500); server.notification::( From f70f256689bfbccdf5b71b133961fc3bb2bc8c21 Mon Sep 17 00:00:00 2001 From: he1pa <18012015693@163.com> Date: Thu, 7 Nov 2024 12:01:12 +0800 Subject: [PATCH 5/5] fix ut Signed-off-by: he1pa <18012015693@163.com> --- .github/workflows/ubuntu_test.yaml | 2 +- kclvm/tools/src/LSP/src/state.rs | 19 ++++++++----------- .../LSP/src/test_data/watcher/modify/main.k | 1 + kclvm/tools/src/LSP/src/tests.rs | 16 +--------------- 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ubuntu_test.yaml b/.github/workflows/ubuntu_test.yaml index 1ca29c7e0..de8fbc61d 100644 --- a/.github/workflows/ubuntu_test.yaml +++ b/.github/workflows/ubuntu_test.yaml @@ -60,8 +60,8 @@ jobs: go install kcl-lang.io/cli/cmd/kcl@main echo "$(go env GOPATH)/bin" >> $GITHUB_PATH echo "${{ github.workspace }}/go/bin" >> $GITHUB_PATH - - name: Unit test working-directory: ./kclvm run: export PATH=$PATH:$PWD/../_build/dist/linux/kclvm/bin && make test shell: bash + diff --git a/kclvm/tools/src/LSP/src/state.rs b/kclvm/tools/src/LSP/src/state.rs index c31e4b950..a74ba395b 100644 --- a/kclvm/tools/src/LSP/src/state.rs +++ b/kclvm/tools/src/LSP/src/state.rs @@ -192,17 +192,13 @@ impl LanguageServerState { for event in self.fs_event_watcher._receiver.try_iter() { if let Ok(e) = event { match e.kind { - notify::EventKind::Modify(modify_kind) => { - if let notify::event::ModifyKind::Data(data_change) = modify_kind { - if let notify::event::DataChange::Content = data_change { - let paths = e.paths; - let kcl_config_file: Vec = filter_kcl_config_file(&paths); - if !kcl_config_file.is_empty() { - return Some(Event::FileWatcher( - FileWatcherEvent::ChangedConfigFile(kcl_config_file), - )); - } - } + notify::EventKind::Modify(_) => { + let paths = e.paths; + let kcl_config_file: Vec = filter_kcl_config_file(&paths); + if !kcl_config_file.is_empty() { + return Some(Event::FileWatcher(FileWatcherEvent::ChangedConfigFile( + kcl_config_file, + ))); } } notify::EventKind::Remove(remove_kind) => { @@ -644,6 +640,7 @@ impl LanguageServerState { &mut files, opts.1.clone(), ); + log_message( format!( "Compile workspace: {:?}, main_pkg files: {:?}, changed file: {:?}, options: {:?}, metadate: {:?}, use {:?} micros", diff --git a/kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k b/kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k index e69de29bb..f5bde4bd0 100644 --- a/kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k +++ b/kclvm/tools/src/LSP/src/test_data/watcher/modify/main.k @@ -0,0 +1 @@ +import helloworld \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index 6dac71010..6de2d1dd9 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -1548,21 +1548,7 @@ fn mod_file_watcher_test() { .unwrap(); // wait for download dependence - wait_async!(500); - - server.notification::( - lsp_types::DidChangeTextDocumentParams { - text_document: lsp_types::VersionedTextDocumentIdentifier { - uri: Url::from_file_path(main_path.clone()).unwrap(), - version: 1, - }, - content_changes: vec![TextDocumentContentChangeEvent { - range: None, - range_length: None, - text: "import helloworld".to_string(), - }], - }, - ); + wait_async!(2000); let id = server.next_request_id.get(); server.next_request_id.set(id.wrapping_add(1));