#[cfg(all(test, feature = "full-test"))]
mod tests;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, AtomicU64, Ordering};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
time::Duration,
};
use super::{ClientProxy, FileDiagnostic};
use crate::context::ServerContextSnapshot;
use crate::context::lsp_features::LspFeatures;
use crate::handlers::{ClientConfig, init_analysis, register_files_watch};
use emmylua_code_analysis::{
EmmyLuaAnalysis, Emmyrc, WorkspaceFileMatcher, WorkspaceFolder, load_configs,
read_file_with_encoding, uri_to_file_path,
};
use lsp_types::Uri;
use tokio::sync::{Mutex as AsyncMutex, RwLock};
use tokio_util::sync::CancellationToken;
pub struct WorkspaceManager {
analysis: Arc<RwLock<EmmyLuaAnalysis>>,
client: Arc<ClientProxy>,
config_reload_token: Arc<PendingTask>,
reindex_token: Arc<PendingTask>,
reload_lock: Arc<AsyncMutex<()>>,
reload_generation: Arc<AtomicU64>,
file_diagnostic: Arc<FileDiagnostic>,
lsp_features: Arc<LspFeatures>,
pub client_config: ClientConfig,
pub workspace_folders: Vec<WorkspaceFolder>,
pub watcher: Option<notify::RecommendedWatcher>,
open_file_texts: HashMap<Uri, String>,
open_file_state_version: u64,
pub match_file_pattern: WorkspaceFileMatcher,
workspace_diagnostic_level: Arc<AtomicU8>,
workspace_version: Arc<AtomicI64>,
}
impl WorkspaceManager {
pub fn new(
analysis: Arc<RwLock<EmmyLuaAnalysis>>,
client: Arc<ClientProxy>,
file_diagnostic: Arc<FileDiagnostic>,
lsp_features: Arc<LspFeatures>,
) -> Self {
Self {
analysis,
client,
config_reload_token: Arc::new(PendingTask::default()),
reindex_token: Arc::new(PendingTask::default()),
reload_lock: Arc::new(AsyncMutex::new(())),
reload_generation: Arc::new(AtomicU64::new(0)),
file_diagnostic,
lsp_features,
client_config: ClientConfig::default(),
workspace_folders: Vec::new(),
watcher: None,
open_file_texts: HashMap::new(),
open_file_state_version: 0,
match_file_pattern: WorkspaceFileMatcher::default(),
workspace_diagnostic_level: Arc::new(AtomicU8::new(
WorkspaceDiagnosticLevel::Fast.to_u8(),
)),
workspace_version: Arc::new(AtomicI64::new(0)),
}
}
pub fn get_workspace_diagnostic_level(&self) -> WorkspaceDiagnosticLevel {
let value = self.workspace_diagnostic_level.load(Ordering::Acquire);
WorkspaceDiagnosticLevel::from_u8(value)
}
pub fn update_workspace_version(&self, level: WorkspaceDiagnosticLevel, add_version: bool) {
self.workspace_diagnostic_level
.store(level.to_u8(), Ordering::Release);
if add_version {
self.workspace_version.fetch_add(1, Ordering::AcqRel);
}
}
pub fn get_workspace_version(&self) -> i64 {
self.workspace_version.load(Ordering::Acquire)
}
pub fn update_match_state(&mut self, emmyrc: &Emmyrc) {
self.match_file_pattern = WorkspaceFileMatcher::new(&self.workspace_folders, emmyrc);
}
pub fn sync_open_file(&mut self, uri: Uri, text: String) {
self.open_file_texts.insert(uri, text);
self.open_file_state_version = self.open_file_state_version.wrapping_add(1);
}
pub fn close_open_file(&mut self, uri: &Uri) {
self.open_file_texts.remove(uri);
self.open_file_state_version = self.open_file_state_version.wrapping_add(1);
}
pub fn is_open_file(&self, uri: &Uri) -> bool {
self.open_file_texts.contains_key(uri)
}
pub fn workspace_open_files(&self) -> Vec<(Uri, String)> {
self.open_file_texts
.iter()
.filter(|(uri, _)| self.is_workspace_file(uri))
.map(|(uri, text)| (uri.clone(), text.clone()))
.collect()
}
fn workspace_open_files_snapshot(&self) -> OpenFilesSnapshot {
OpenFilesSnapshot {
version: self.open_file_state_version,
files: self.workspace_open_files(),
}
}
pub fn add_update_emmyrc_task(&self, context: ServerContextSnapshot, config_path: PathBuf) {
let Some(config_root) = self.config_root() else {
return;
};
if config_path.parent() != Some(config_root.as_path()) {
return;
}
let (cancel_token, cancelled_existing) =
self.config_reload_token.replace(CONFIG_RELOAD_DELAY);
if cancelled_existing {
log::debug!("cancel pending config reload: {:?}", config_path);
}
let workspace_folders = self.workspace_folders.clone();
let client_config = self.client_config.clone();
let config_reload_token = self.config_reload_token.clone();
let reload_task_handles = self.reload_task_handles();
tokio::spawn(async move {
cancel_token.wait().await;
if cancel_token.is_cancelled() {
config_reload_token.clear(&cancel_token);
return;
}
let emmyrc = load_emmy_config(Some(config_root), client_config);
spawn_workspace_reload_task(reload_task_handles, context, workspace_folders, emmyrc);
config_reload_token.clear(&cancel_token);
});
}
pub fn add_reload_workspace_task(&self, context: ServerContextSnapshot) {
let emmyrc = load_emmy_config(self.config_root(), self.client_config.clone());
spawn_workspace_reload_task(
self.reload_task_handles(),
context,
self.workspace_folders.clone(),
emmyrc,
);
}
pub fn extend_reindex_delay(&self) {
if let Some(token) = self.reindex_token.current() {
token.set_resleep();
}
}
pub fn reindex_workspace(&self, delay: Duration) {
log::info!("reindex workspace with delay: {:?}", delay);
let (cancel_token, cancelled_existing) = self.reindex_token.replace(delay);
if cancelled_existing {
log::info!("cancel reindex workspace");
}
let analysis = self.analysis.clone();
let client = self.client.clone();
let file_diagnostic = self.file_diagnostic.clone();
let lsp_features = self.lsp_features.clone();
let reindex_token = self.reindex_token.clone();
let workspace_diagnostic_level = self.workspace_diagnostic_level.clone();
tokio::spawn(async move {
cancel_token.wait().await;
if cancel_token.is_cancelled() {
reindex_token.clear(&cancel_token);
return;
}
{
let mut analysis = analysis.write().await;
analysis.cleanup_nonexistent_files();
analysis.reindex();
drop(analysis);
}
refresh_workspace_diagnostics(
file_diagnostic,
lsp_features,
client,
workspace_diagnostic_level,
)
.await;
reindex_token.clear(&cancel_token);
});
}
pub fn is_workspace_file(&self, uri: &Uri) -> bool {
if self.workspace_folders.is_empty() {
return true;
}
let Some(file_path) = uri_to_file_path(uri) else {
return true;
};
self.match_file_pattern.is_match(&file_path)
}
pub async fn check_schema_update(&self) {
let read_analysis = self.analysis.read().await;
if read_analysis.check_schema_update() {
drop(read_analysis);
let mut write_analysis = self.analysis.write().await;
write_analysis.update_schema().await;
}
}
fn config_root(&self) -> Option<PathBuf> {
self.workspace_folders
.first()
.map(|workspace| workspace.root.clone())
}
fn reload_task_handles(&self) -> ReloadTaskHandles {
ReloadTaskHandles {
client: self.client.clone(),
file_diagnostic: self.file_diagnostic.clone(),
lsp_features: self.lsp_features.clone(),
reload_lock: self.reload_lock.clone(),
reload_generation: self.reload_generation.clone(),
workspace_diagnostic_level: self.workspace_diagnostic_level.clone(),
}
}
}
const CONFIG_FILE_NAMES: [&str; 3] = [".luarc.json", ".emmyrc.json", ".emmyrc.lua"];
const CONFIG_RELOAD_DELAY: Duration = Duration::from_secs(2);
pub fn load_emmy_config(config_root: Option<PathBuf>, client_config: ClientConfig) -> Arc<Emmyrc> {
let mut config_files = Vec::new();
extend_config_files(&mut config_files, dirs::home_dir());
extend_config_files(
&mut config_files,
dirs::config_dir().map(|path| path.join("emmylua_ls")),
);
if let Ok(path) = std::env::var("EMMYLUALS_CONFIG") {
let path = PathBuf::from(path);
if path.exists() {
log::info!("load config from: {:?}", path);
config_files.push(path);
}
}
extend_config_files(&mut config_files, config_root.clone());
let mut emmyrc = load_configs(config_files, client_config.partial_emmyrcs.clone());
merge_client_config(client_config, &mut emmyrc);
if let Some(workspace_root) = &config_root {
emmyrc.pre_process_emmyrc(workspace_root);
}
log::info!("loaded emmyrc complete");
emmyrc.into()
}
fn merge_client_config(client_config: ClientConfig, emmyrc: &mut Emmyrc) -> Option<()> {
emmyrc.runtime.extensions.extend(client_config.extensions);
emmyrc.workspace.ignore_globs.extend(client_config.exclude);
if client_config.encoding != "utf-8" {
emmyrc.workspace.encoding = client_config.encoding;
}
Some(())
}
fn extend_config_files(config_files: &mut Vec<PathBuf>, dir: Option<PathBuf>) {
let Some(dir) = dir else {
return;
};
for file_name in CONFIG_FILE_NAMES {
let path = dir.join(file_name);
if path.exists() {
log::info!("load config from: {:?}", path);
config_files.push(path);
}
}
}
#[derive(Debug)]
struct DebounceToken {
cancel_token: CancellationToken,
time_sleep: Duration,
need_re_sleep: AtomicBool,
}
impl DebounceToken {
fn new(time_sleep: Duration) -> Self {
Self {
cancel_token: CancellationToken::new(),
time_sleep,
need_re_sleep: AtomicBool::new(false),
}
}
async fn wait(&self) {
loop {
tokio::select! {
_ = tokio::time::sleep(self.time_sleep) => {
if !self.need_re_sleep.swap(false, Ordering::AcqRel) {
break;
}
}
_ = self.cancel_token.cancelled() => break,
}
}
}
fn cancel(&self) {
self.cancel_token.cancel();
}
fn is_cancelled(&self) -> bool {
self.cancel_token.is_cancelled()
}
fn set_resleep(&self) {
self.need_re_sleep.store(true, Ordering::Release);
}
}
#[derive(Debug, Default)]
struct PendingTask(Mutex<Option<Arc<DebounceToken>>>);
impl PendingTask {
fn current(&self) -> Option<Arc<DebounceToken>> {
self.0.lock().expect("update token mutex poisoned").clone()
}
fn replace(&self, delay: Duration) -> (Arc<DebounceToken>, bool) {
let mut current = self.0.lock().expect("update token mutex poisoned");
let had_existing_token = current.is_some();
if let Some(token) = current.as_ref() {
token.cancel();
}
let next = Arc::new(DebounceToken::new(delay));
current.replace(next.clone());
(next, had_existing_token)
}
fn clear(&self, finished_token: &Arc<DebounceToken>) {
let mut current = self.0.lock().expect("update token mutex poisoned");
if current
.as_ref()
.is_some_and(|token| Arc::ptr_eq(token, finished_token))
{
current.take();
}
}
}
async fn refresh_workspace_diagnostics(
file_diagnostic: Arc<FileDiagnostic>,
lsp_features: Arc<LspFeatures>,
client: Arc<ClientProxy>,
workspace_diagnostic_level: Arc<AtomicU8>,
) {
file_diagnostic.cancel_workspace_diagnostic().await;
workspace_diagnostic_level.store(WorkspaceDiagnosticLevel::Fast.to_u8(), Ordering::Release);
if lsp_features.supports_workspace_diagnostic() {
client.refresh_workspace_diagnostics();
} else {
file_diagnostic
.add_workspace_diagnostic_task(500, true)
.await;
}
}
#[derive(Clone)]
struct ReloadTaskHandles {
client: Arc<ClientProxy>,
file_diagnostic: Arc<FileDiagnostic>,
lsp_features: Arc<LspFeatures>,
reload_lock: Arc<AsyncMutex<()>>,
reload_generation: Arc<AtomicU64>,
workspace_diagnostic_level: Arc<AtomicU8>,
}
fn spawn_workspace_reload_task(
handles: ReloadTaskHandles,
context: ServerContextSnapshot,
workspace_folders: Vec<WorkspaceFolder>,
emmyrc: Arc<Emmyrc>,
) {
let generation = handles.reload_generation.fetch_add(1, Ordering::AcqRel) + 1;
tokio::spawn(async move {
let _reload_guard = handles.reload_lock.lock().await;
if generation != handles.reload_generation.load(Ordering::Acquire) {
return;
}
apply_workspace_reload(context, workspace_folders, emmyrc).await;
if generation != handles.reload_generation.load(Ordering::Acquire) {
return;
}
refresh_workspace_diagnostics(
handles.file_diagnostic,
handles.lsp_features,
handles.client,
handles.workspace_diagnostic_level,
)
.await;
});
}
async fn apply_workspace_reload(
context: ServerContextSnapshot,
workspace_folders: Vec<WorkspaceFolder>,
emmyrc: Arc<Emmyrc>,
) {
let open_files = {
let mut workspace_manager = context.workspace_manager().write().await;
workspace_manager.update_match_state(emmyrc.as_ref());
workspace_manager.workspace_open_files_snapshot()
};
{
let mut analysis = context.analysis().write().await;
analysis.clear_non_std_workspaces();
}
init_analysis(
context.analysis(),
context.status_bar(),
context.file_diagnostic(),
context.lsp_features(),
workspace_folders,
emmyrc,
open_files.files.clone(),
)
.await;
sync_reloaded_open_files(context.clone(), open_files).await;
register_files_watch(context).await;
}
async fn sync_reloaded_open_files(
context: ServerContextSnapshot,
mut applied_snapshot: OpenFilesSnapshot,
) {
loop {
let snapshot_update = {
let workspace_manager = context.workspace_manager().read().await;
let next_snapshot = workspace_manager.workspace_open_files_snapshot();
if next_snapshot.version == applied_snapshot.version {
None
} else {
let next_open_uris = next_snapshot
.files
.iter()
.map(|(uri, _)| uri.clone())
.collect::<HashSet<_>>();
let removed_actions = applied_snapshot
.files
.iter()
.filter_map(|(uri, _)| {
if next_open_uris.contains(uri) {
return None;
}
if workspace_manager.is_workspace_file(uri)
&& let Some(path) = uri_to_file_path(uri)
&& path.exists()
{
return Some(OpenFileSyncAction::RestoreFromDisk(uri.clone(), path));
}
Some(OpenFileSyncAction::Remove(uri.clone()))
})
.collect::<Vec<_>>();
Some((next_snapshot, removed_actions))
}
};
let Some((next_snapshot, removed_actions)) = snapshot_update else {
return;
};
let removed_uris = apply_open_file_sync(
context.analysis(),
next_snapshot.files.clone(),
removed_actions,
)
.await;
if !context.lsp_features().supports_pull_diagnostic() {
for uri in removed_uris {
context.file_diagnostic().clear_push_file_diagnostics(uri);
}
}
applied_snapshot = next_snapshot;
}
}
async fn apply_open_file_sync(
analysis: &RwLock<EmmyLuaAnalysis>,
current_open_files: Vec<(Uri, String)>,
removed_actions: Vec<OpenFileSyncAction>,
) -> Vec<Uri> {
if current_open_files.is_empty() && removed_actions.is_empty() {
return Vec::new();
}
let mut analysis = analysis.write().await;
let encoding = analysis.get_emmyrc().workspace.encoding.clone();
let mut updates = current_open_files
.into_iter()
.map(|(uri, text)| (uri, Some(text)))
.collect::<Vec<_>>();
let mut removed_uris = Vec::new();
for action in removed_actions {
match action {
OpenFileSyncAction::RestoreFromDisk(uri, path) => {
if let Some(text) = read_file_with_encoding(&path, &encoding) {
updates.push((uri, Some(text)));
} else {
analysis.remove_file_by_uri(&uri);
removed_uris.push(uri);
}
}
OpenFileSyncAction::Remove(uri) => {
analysis.remove_file_by_uri(&uri);
removed_uris.push(uri);
}
}
}
if !updates.is_empty() {
analysis.update_files_by_uri(updates);
}
removed_uris
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceDiagnosticLevel {
None = 0,
Fast = 1,
Slow = 2,
}
impl WorkspaceDiagnosticLevel {
pub fn from_u8(value: u8) -> Self {
match value {
1 => Self::Fast,
2 => Self::Slow,
_ => Self::None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Default)]
struct OpenFilesSnapshot {
version: u64,
files: Vec<(Uri, String)>,
}
#[derive(Debug, Clone)]
enum OpenFileSyncAction {
RestoreFromDisk(Uri, PathBuf),
Remove(Uri),
}