glua_ls 1.0.27

Language server for Garry's Mod Lua (GLua).
Documentation
use glua_code_analysis::file_path_to_uri;
use log::{info, warn};
use lsp_types::{
    ClientCapabilities, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
    FileEvent, FileSystemWatcher, GlobPattern, Registration, RegistrationParams, WatchKind,
};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::{sync::mpsc::channel, time::Duration};

use crate::{
    context::{ClientProxy, ServerContextSnapshot},
    handlers::text_document::on_did_change_watched_files,
};

pub async fn register_files_watch(
    context: ServerContextSnapshot,
    client_capabilities: &ClientCapabilities,
) {
    let lsp_client_can_watch_files = is_lsp_client_can_watch_files(client_capabilities);

    if lsp_client_can_watch_files {
        register_files_watch_use_lsp_client(context.client());
    } else {
        info!("use notify to watch files");
        register_files_watch_use_fsnotify(context).await;
    }
}

fn is_lsp_client_can_watch_files(client_capabilities: &ClientCapabilities) -> bool {
    client_capabilities
        .workspace
        .as_ref()
        .and_then(|ws| ws.did_change_watched_files.as_ref())
        .and_then(|d| d.dynamic_registration)
        .unwrap_or_default()
}

fn register_files_watch_use_lsp_client(client: &ClientProxy) {
    let options = DidChangeWatchedFilesRegistrationOptions {
        watchers: vec![
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/*.lua".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/.editorconfig".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/.luarc.json".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/.emmyrc.json".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/.emmyrc.lua".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
            FileSystemWatcher {
                glob_pattern: GlobPattern::String("**/.gluarc.json".into()),
                kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
            },
        ],
    };

    let registration = Registration {
        id: "gluals_watch_files".to_string(),
        method: "workspace/didChangeWatchedFiles".to_string(),
        register_options: Some(serde_json::to_value(options).unwrap()),
    };
    client.dynamic_register_capability(RegistrationParams {
        registrations: vec![registration],
    });
}

const WATCH_FILE_EXTENSIONS: [&str; 6] = [
    ".lua",
    ".editorconfig",
    ".luarc.json",
    ".emmyrc.json",
    ".emmyrc.lua",
    ".gluarc.json",
];

async fn register_files_watch_use_fsnotify(context: ServerContextSnapshot) -> Option<()> {
    let (tx, rx) = channel();
    let config = Config::default().with_poll_interval(Duration::from_secs(5));
    let mut watcher = match RecommendedWatcher::new(
        move |res| {
            if let Ok(event) = res {
                match tx.send(event) {
                    Ok(_) => {}
                    Err(e) => {
                        warn!("send notify event failed: {:?}", e);
                    }
                };
            };
        },
        config,
    ) {
        Ok(watcher) => watcher,
        Err(e) => {
            log::error!("create notify watcher failed: {:?}", e);
            return None;
        }
    };

    let mut workspace_manager = context.workspace_manager().write().await;
    for workspace in &workspace_manager.workspace_folders {
        if let Err(e) = watcher.watch(&workspace.root, RecursiveMode::Recursive) {
            warn!("can not watch {:?}: {:?}", workspace.root, e);
        }
    }
    workspace_manager.watcher = Some(watcher);
    drop(workspace_manager);

    tokio::spawn(async move {
        loop {
            match rx.recv() {
                Ok(event) => {
                    let typ = match event.kind {
                        notify::event::EventKind::Create(_) => lsp_types::FileChangeType::CREATED,
                        notify::event::EventKind::Modify(_) => lsp_types::FileChangeType::CHANGED,
                        notify::event::EventKind::Remove(_) => lsp_types::FileChangeType::DELETED,
                        _ => {
                            break;
                        }
                    };
                    let mut file_events = vec![];
                    for path in event.paths.iter() {
                        for ext in WATCH_FILE_EXTENSIONS.iter() {
                            if path.as_os_str().to_string_lossy().ends_with(ext) {
                                if let Some(uri) = file_path_to_uri(path) {
                                    file_events.push(FileEvent { uri, typ });
                                }
                                break;
                            }
                        }
                    }

                    if file_events.is_empty() {
                        continue;
                    }
                    let params = DidChangeWatchedFilesParams {
                        changes: file_events,
                    };
                    on_did_change_watched_files(context.clone(), params).await;
                }
                Err(e) => {
                    warn!("watch files notify error: {:?}", e);
                    break;
                }
            }
        }
    });

    info!("watch files use notify success");

    Some(())
}