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] 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) {