325 lines · 10.1 KB · Rust
RawDownload
use anyhow::Result;use async_trait::async_trait;use collections::HashMap;use language::{LanguageServerName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};use lsp::LanguageServerBinary;use node_runtime::{NodeRuntime, VersionStrategy};use project::ContextProviderWithTasks;use semver::Version;use std::{future::Future, path::PathBuf, sync::Arc, vec};use task::{TaskTemplate, TaskTemplates, VariableName};use util::{ResultExt, maybe}; pub(super) fn bash_task_context() -> ContextProviderWithTasks {    ContextProviderWithTasks::new(TaskTemplates(vec![        TaskTemplate {            label: "execute selection".to_owned(),            command: VariableName::SelectedText.template_value(),            ..TaskTemplate::default()        },        TaskTemplate {            label: format!("run '{}'", VariableName::File.template_value()),            command: VariableName::File.template_value(),            tags: vec!["bash-script".to_owned()],            ..TaskTemplate::default()        },    ]))} pub struct BashLspAdapter {    node: NodeRuntime,} impl BashLspAdapter {    const PACKAGE_NAME: &str = "bash-language-server";    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "bash-language-server/out/cli.js";     pub fn new(node: NodeRuntime) -> Self {        Self { node }    }     async fn get_cached_server_binary(        container_dir: PathBuf,        env: HashMap<String, String>,        node: &NodeRuntime,    ) -> Option<lsp::LanguageServerBinary> {        maybe!(async {            let server_path = container_dir                .join("node_modules")                .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);            anyhow::ensure!(                server_path.exists(),                "missing executable in directory {server_path:?}"            );            Ok(LanguageServerBinary {                path: node.binary_path().await?,                env: Some(env),                arguments: vec![server_path.into(), "start".into()],            })        })        .await        .log_err()    }} impl LspInstaller for BashLspAdapter {    type BinaryVersion = Version;     async fn cached_server_binary(        &self,        container_dir: std::path::PathBuf,        delegate: &dyn LspAdapterDelegate,    ) -> Option<lsp::LanguageServerBinary> {        let env = delegate.shell_env().await;        Self::get_cached_server_binary(container_dir, env, &self.node).await    }     async fn check_if_user_installed(        &self,        delegate: &dyn LspAdapterDelegate,        _: Option<Toolchain>,        _: &gpui::AsyncApp,    ) -> Option<lsp::LanguageServerBinary> {        let path = delegate.which(Self::PACKAGE_NAME.as_ref()).await?;        let env = delegate.shell_env().await;         Some(LanguageServerBinary {            path,            env: Some(env),            arguments: vec!["start".into()],        })    }     fn check_if_version_installed(        &self,        version: &Self::BinaryVersion,        container_dir: &PathBuf,        delegate: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Option<lsp::LanguageServerBinary>> + use<> {        let node = self.node.clone();        let version = version.clone();        let container_dir = container_dir.clone();        let delegate = delegate.clone();         async move {            let server_path = container_dir                .join("node_modules")                .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);             let should_install_language_server = node                .should_install_npm_package(                    Self::PACKAGE_NAME,                    &server_path,                    &container_dir,                    VersionStrategy::Latest(&version),                )                .await;             if should_install_language_server {                None            } else {                let env = delegate.shell_env().await;                Some(LanguageServerBinary {                    path: node.binary_path().await.ok()?,                    env: Some(env),                    arguments: vec![server_path.into(), "start".into()],                })            }        }    }     async fn fetch_latest_server_version(        &self,        _: &dyn LspAdapterDelegate,        _: bool,        _: &mut gpui::AsyncApp,    ) -> Result<Self::BinaryVersion> {        self.node            .npm_package_latest_version(Self::PACKAGE_NAME)            .await    }     fn fetch_server_binary(        &self,        latest_version: Self::BinaryVersion,        container_dir: std::path::PathBuf,        delegate: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Result<lsp::LanguageServerBinary>> + use<> {        let node = self.node.clone();        let delegate = delegate.clone();         async move {            let server_path = container_dir                .join("node_modules")                .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);            let latest_version = latest_version.to_string();             node.npm_install_packages(                &container_dir,                &[(Self::PACKAGE_NAME, latest_version.as_str())],            )            .await?;             let env = delegate.shell_env().await;            Ok(LanguageServerBinary {                path: node.binary_path().await?,                env: Some(env),                arguments: vec![server_path.into(), "start".into()],            })        }    }} #[async_trait(?Send)]impl LspAdapter for BashLspAdapter {    fn name(&self) -> LanguageServerName {        LanguageServerName::new_static(Self::PACKAGE_NAME)    }} #[cfg(test)]mod tests {    use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};    use language::{AutoindentMode, Buffer};    use settings::SettingsStore;    use std::num::NonZeroU32;    use unindent::Unindent;    use util::test::marked_text_offsets;     #[gpui::test]    async fn test_bash_autoindent(cx: &mut TestAppContext) {        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);        let language = crate::language("bash", tree_sitter_bash::LANGUAGE.into());        cx.update(|cx| {            let test_settings = SettingsStore::test(cx);            cx.set_global(test_settings);            cx.update_global::<SettingsStore, _>(|store, cx| {                store.update_user_settings(cx, |s| {                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2)                });            });        });         cx.new(|cx| {            let mut buffer = Buffer::local("", cx).with_language(language, cx);             let expect_indents_to =                |buffer: &mut Buffer, cx: &mut Context<Buffer>, input: &str, expected: &str| {                    buffer.edit(                        [(0..buffer.len(), input)],                        Some(AutoindentMode::EachLine),                        cx,                    );                    assert_eq!(buffer.text(), expected);                };             // Do not indent after shebang            expect_indents_to(                &mut buffer,                cx,                "#!/usr/bin/env bash\n#",                "#!/usr/bin/env bash\n#",            );             // indent function correctly            expect_indents_to(                &mut buffer,                cx,                "function name() {\necho \"Hello, World!\"\n}",                "function name() {\n  echo \"Hello, World!\"\n}",            );             // indent if-else correctly            expect_indents_to(                &mut buffer,                cx,                "if true;then\nfoo\nelse\nbar\nfi",                "if true;then\n  foo\nelse\n  bar\nfi",            );             // indent if-elif-else correctly            expect_indents_to(                &mut buffer,                cx,                "if true;then\nfoo\nelif true;then\nbar\nelse\nbar\nfi",                "if true;then\n  foo\nelif true;then\n  bar\nelse\n  bar\nfi",            );             // indent case-when-else correctly            expect_indents_to(                &mut buffer,                cx,                "case $1 in\nfoo) echo \"Hello, World!\";;\n*) echo \"Unknown argument\";;\nesac",                "case $1 in\n  foo) echo \"Hello, World!\";;\n  *) echo \"Unknown argument\";;\nesac",            );             // indent for-loop correctly            expect_indents_to(                &mut buffer,                cx,                "for i in {1..10};do\nfoo\ndone",                "for i in {1..10};do\n  foo\ndone",            );             // indent while-loop correctly            expect_indents_to(                &mut buffer,                cx,                "while true; do\nfoo\ndone",                "while true; do\n  foo\ndone",            );             // indent array correctly            expect_indents_to(                &mut buffer,                cx,                "array=(\n1\n2\n3\n)",                "array=(\n  1\n  2\n  3\n)",            );             // indents non-"function" function correctly            expect_indents_to(                &mut buffer,                cx,                "foo() {\necho \"Hello, World!\"\n}",                "foo() {\n  echo \"Hello, World!\"\n}",            );             let (input, offsets) = marked_text_offsets(                &r#"                if foo; then                  1ˇ                else                  3                fi                "#                .unindent(),            );             buffer.edit([(0..buffer.len(), input)], None, cx);            buffer.edit(                [(offsets[0]..offsets[0], "\n")],                Some(AutoindentMode::EachLine),                cx,            );            buffer.edit(                [(offsets[0] + 3..offsets[0] + 3, "elif")],                Some(AutoindentMode::EachLine),                cx,            );            let expected = r#"                if foo; then                  1                elif                else                  3                fi                "#            .unindent();             pretty_assertions::assert_eq!(buffer.text(), expected);             buffer        });    }}