shuck-server 0.0.42

Language server scaffold for shuck
Documentation
use lsp_types as types;
use lsp_types::notification as notif;

use crate::server::Result;
use crate::session::{Client, Session};

pub(crate) struct DidChangeWatchedFiles;

impl super::super::traits::NotificationHandler for DidChangeWatchedFiles {
    type NotificationType = notif::DidChangeWatchedFiles;
}

impl super::super::traits::SyncNotificationHandler for DidChangeWatchedFiles {
    fn run(
        session: &mut Session,
        client: &Client,
        params: types::DidChangeWatchedFilesParams,
    ) -> Result<()> {
        session.reload_settings(&params.changes, client);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crossbeam::channel;
    use lsp_types::{
        ClientCapabilities, DidChangeWatchedFilesClientCapabilities, FileChangeType, FileEvent,
        Url, WorkspaceClientCapabilities,
    };

    use super::*;
    use crate::server::api::traits::SyncNotificationHandler;
    use crate::{
        Client, GlobalOptions, PositionEncoding, Session, TextDocument, Workspace, Workspaces,
    };

    fn client_capabilities_with_dynamic_watched_files() -> ClientCapabilities {
        ClientCapabilities {
            workspace: Some(WorkspaceClientCapabilities {
                did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
                    dynamic_registration: Some(true),
                    relative_pattern_support: None,
                }),
                ..WorkspaceClientCapabilities::default()
            }),
            ..ClientCapabilities::default()
        }
    }

    #[test]
    fn config_changes_are_reflected_on_the_next_snapshot() {
        let workspace_root = tempfile::tempdir().expect("tempdir should be created");
        let config_path = workspace_root.path().join(".shuck.toml");
        std::fs::write(&config_path, "[lint]\nselect = ['C001']\n")
            .expect("config should be written");
        let file_path = workspace_root.path().join("script.sh");
        std::fs::write(&file_path, "foo=1\n").expect("source should be written");

        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
        let (client_sender, _client_receiver) = channel::unbounded();
        let client = Client::new(main_loop_sender, client_sender);
        let workspace_uri =
            Url::from_file_path(workspace_root.path()).expect("workspace path should convert");
        let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
        let global = GlobalOptions::default().into_settings(client.clone());
        let mut session = Session::new(
            &client_capabilities_with_dynamic_watched_files(),
            PositionEncoding::UTF16,
            global,
            &workspaces,
            &client,
        )
        .expect("test session should initialize");
        session.set_project_settings_cache_enabled(true);
        let uri = Url::from_file_path(&file_path).expect("script path should convert to a URL");
        session.open_text_document(
            uri.clone(),
            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
        );

        let before = session
            .take_snapshot(uri.clone())
            .expect("test document should produce a snapshot");
        assert!(
            before
                .shuck_settings()
                .linter()
                .rules
                .contains(shuck_linter::Rule::UnusedAssignment)
        );
        assert_eq!(before.shuck_settings().linter().rules.len(), 1);

        std::fs::write(&config_path, "[lint]\nselect = ['C006']\n")
            .expect("config should be updated");

        let cached = session
            .take_snapshot(uri.clone())
            .expect("settings should stay cached until invalidated");
        assert!(
            cached
                .shuck_settings()
                .linter()
                .rules
                .contains(shuck_linter::Rule::UnusedAssignment)
        );
        assert_eq!(cached.shuck_settings().linter().rules.len(), 1);

        DidChangeWatchedFiles::run(
            &mut session,
            &client,
            types::DidChangeWatchedFilesParams {
                changes: vec![FileEvent {
                    uri: Url::from_file_path(&config_path)
                        .expect("config path should convert to a URL"),
                    typ: FileChangeType::CHANGED,
                }],
            },
        )
        .expect("didChangeWatchedFiles should succeed");

        let after = session
            .take_snapshot(uri)
            .expect("test document should produce a snapshot");
        assert!(
            after
                .shuck_settings()
                .linter()
                .rules
                .contains(shuck_linter::Rule::UndefinedVariable)
        );
        assert_eq!(after.shuck_settings().linter().rules.len(), 1);
    }

    #[test]
    fn config_changes_refresh_without_dynamic_file_watch_support() {
        let workspace_root = tempfile::tempdir().expect("tempdir should be created");
        let config_path = workspace_root.path().join(".shuck.toml");
        std::fs::write(&config_path, "[lint]\nselect = ['C001']\n")
            .expect("config should be written");
        let file_path = workspace_root.path().join("script.sh");
        std::fs::write(&file_path, "foo=1\n").expect("source should be written");

        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
        let (client_sender, _client_receiver) = channel::unbounded();
        let client = Client::new(main_loop_sender, client_sender);
        let workspace_uri =
            Url::from_file_path(workspace_root.path()).expect("workspace path should convert");
        let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
        let global = GlobalOptions::default().into_settings(client.clone());
        let mut session = Session::new(
            &ClientCapabilities::default(),
            PositionEncoding::UTF16,
            global,
            &workspaces,
            &client,
        )
        .expect("test session should initialize");
        let uri = Url::from_file_path(&file_path).expect("script path should convert to a URL");
        session.open_text_document(
            uri.clone(),
            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
        );

        let before = session
            .take_snapshot(uri.clone())
            .expect("test document should produce a snapshot");
        assert!(
            before
                .shuck_settings()
                .linter()
                .rules
                .contains(shuck_linter::Rule::UnusedAssignment)
        );
        assert_eq!(before.shuck_settings().linter().rules.len(), 1);

        std::fs::write(&config_path, "[lint]\nselect = ['C006']\n")
            .expect("config should be updated");

        let after = session
            .take_snapshot(uri)
            .expect("test document should produce a snapshot");
        assert!(
            after
                .shuck_settings()
                .linter()
                .rules
                .contains(shuck_linter::Rule::UndefinedVariable)
        );
        assert_eq!(after.shuck_settings().linter().rules.len(), 1);
    }
}