1345 lines · 45.8 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use async_trait::async_trait;use collections::HashMap;use futures::StreamExt;use gpui::{App, AsyncApp, Task};use http_client::github::latest_github_release;pub use language::*;use language::{LanguageToolchainStore, LspAdapterDelegate, LspInstaller};use lsp::{LanguageServerBinary, LanguageServerName}; use regex::Regex;use serde_json::json;use smol::fs;use std::{    borrow::Cow,    ffi::{OsStr, OsString},    ops::Range,    path::{Path, PathBuf},    process::Output,    str,    sync::{        Arc, LazyLock,        atomic::{AtomicBool, Ordering::SeqCst},    },};use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};use util::{ResultExt, fs::remove_matching, maybe}; fn server_binary_arguments() -> Vec<OsString> {    vec!["-mode=stdio".into()]} #[derive(Copy, Clone)]pub struct GoLspAdapter; impl GoLspAdapter {    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");} static VERSION_REGEX: LazyLock<Regex> =    LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX")); static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {    Regex::new(r#"[.*+?^${}()|\[\]\\"']"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")}); const BINARY: &str = if cfg!(target_os = "windows") {    "gopls.exe"} else {    "gopls"}; impl LspInstaller for GoLspAdapter {    type BinaryVersion = Option<String>;     async fn fetch_latest_server_version(        &self,        delegate: &dyn LspAdapterDelegate,        _: bool,        cx: &mut AsyncApp,    ) -> Result<Option<String>> {        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);         const NOTIFICATION_MESSAGE: &str =            "Could not install the Go language server `gopls`, because `go` was not found.";         if delegate.which("go".as_ref()).await.is_none() {            if DID_SHOW_NOTIFICATION                .compare_exchange(false, true, SeqCst, SeqCst)                .is_ok()            {                cx.update(|cx| {                    delegate.show_notification(NOTIFICATION_MESSAGE, cx);                })?            }            anyhow::bail!(                "Could not install the Go language server `gopls`, because `go` was not found."            );        }         let release =            latest_github_release("golang/tools", false, false, delegate.http_client()).await?;        let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);        if version.is_none() {            log::warn!(                "couldn't infer gopls version from GitHub release tag name '{}'",                release.tag_name            );        }        Ok(version)    }     async fn check_if_user_installed(        &self,        delegate: &dyn LspAdapterDelegate,        _: Option<Toolchain>,        _: &AsyncApp,    ) -> Option<LanguageServerBinary> {        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;        Some(LanguageServerBinary {            path,            arguments: server_binary_arguments(),            env: None,        })    }     async fn fetch_server_binary(        &self,        version: Option<String>,        container_dir: PathBuf,        delegate: &dyn LspAdapterDelegate,    ) -> Result<LanguageServerBinary> {        let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());        let go_version_output = util::command::new_smol_command(&go)            .args(["version"])            .output()            .await            .context("failed to get go version via `go version` command`")?;        let go_version = parse_version_output(&go_version_output)?;         if let Some(version) = version {            let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));            if let Ok(metadata) = fs::metadata(&binary_path).await                && metadata.is_file()            {                remove_matching(&container_dir, |entry| {                    entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))                })                .await;                 return Ok(LanguageServerBinary {                    path: binary_path.to_path_buf(),                    arguments: server_binary_arguments(),                    env: None,                });            }        } else if let Some(path) = get_cached_server_binary(&container_dir).await {            return Ok(path);        }         let gobin_dir = container_dir.join("gobin");        fs::create_dir_all(&gobin_dir).await?;        let install_output = util::command::new_smol_command(go)            .env("GO111MODULE", "on")            .env("GOBIN", &gobin_dir)            .args(["install", "golang.org/x/tools/gopls@latest"])            .output()            .await?;         if !install_output.status.success() {            log::error!(                "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",                String::from_utf8_lossy(&install_output.stdout),                String::from_utf8_lossy(&install_output.stderr)            );            anyhow::bail!(                "failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."            );        }         let installed_binary_path = gobin_dir.join(BINARY);        let version_output = util::command::new_smol_command(&installed_binary_path)            .arg("version")            .output()            .await            .context("failed to run installed gopls binary")?;        let gopls_version = parse_version_output(&version_output)?;        let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));        fs::rename(&installed_binary_path, &binary_path).await?;         Ok(LanguageServerBinary {            path: binary_path.to_path_buf(),            arguments: server_binary_arguments(),            env: None,        })    }     async fn cached_server_binary(        &self,        container_dir: PathBuf,        _: &dyn LspAdapterDelegate,    ) -> Option<LanguageServerBinary> {        get_cached_server_binary(&container_dir).await    }} #[async_trait(?Send)]impl LspAdapter for GoLspAdapter {    fn name(&self) -> LanguageServerName {        Self::SERVER_NAME    }     async fn initialization_options(        self: Arc<Self>,        _: &Arc<dyn LspAdapterDelegate>,    ) -> Result<Option<serde_json::Value>> {        Ok(Some(json!({            "usePlaceholders": false,            "hints": {                "assignVariableTypes": true,                "compositeLiteralFields": true,                "compositeLiteralTypes": true,                "constantValues": true,                "functionTypeParameters": true,                "parameterNames": true,                "rangeVariableTypes": true            }        })))    }     async fn label_for_completion(        &self,        completion: &lsp::CompletionItem,        language: &Arc<Language>,    ) -> Option<CodeLabel> {        let label = &completion.label;         // Gopls returns nested fields and methods as completions.        // To syntax highlight these, combine their final component        // with their detail.        let name_offset = label.rfind('.').unwrap_or(0);         match completion.kind.zip(completion.detail.as_ref()) {            Some((lsp::CompletionItemKind::MODULE, detail)) => {                let text = format!("{label} {detail}");                let source = Rope::from(format!("import {text}").as_str());                let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());                let filter_range = completion                    .filter_text                    .as_deref()                    .and_then(|filter_text| {                        text.find(filter_text)                            .map(|start| start..start + filter_text.len())                    })                    .unwrap_or(0..label.len());                return Some(CodeLabel::new(text, filter_range, runs));            }            Some((                lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,                detail,            )) => {                let text = format!("{label} {detail}");                let source =                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());                let runs = adjust_runs(                    name_offset,                    language.highlight_text(&source, 4..4 + text[name_offset..].len()),                );                let filter_range = completion                    .filter_text                    .as_deref()                    .and_then(|filter_text| {                        text.find(filter_text)                            .map(|start| start..start + filter_text.len())                    })                    .unwrap_or(0..label.len());                return Some(CodeLabel::new(text, filter_range, runs));            }            Some((lsp::CompletionItemKind::STRUCT, _)) => {                let text = format!("{label} struct {{}}");                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());                let runs = adjust_runs(                    name_offset,                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),                );                let filter_range = completion                    .filter_text                    .as_deref()                    .and_then(|filter_text| {                        text.find(filter_text)                            .map(|start| start..start + filter_text.len())                    })                    .unwrap_or(0..label.len());                return Some(CodeLabel::new(text, filter_range, runs));            }            Some((lsp::CompletionItemKind::INTERFACE, _)) => {                let text = format!("{label} interface {{}}");                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());                let runs = adjust_runs(                    name_offset,                    language.highlight_text(&source, 5..5 + text[name_offset..].len()),                );                let filter_range = completion                    .filter_text                    .as_deref()                    .and_then(|filter_text| {                        text.find(filter_text)                            .map(|start| start..start + filter_text.len())                    })                    .unwrap_or(0..label.len());                return Some(CodeLabel::new(text, filter_range, runs));            }            Some((lsp::CompletionItemKind::FIELD, detail)) => {                let text = format!("{label} {detail}");                let source =                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());                let runs = adjust_runs(                    name_offset,                    language.highlight_text(&source, 16..16 + text[name_offset..].len()),                );                let filter_range = completion                    .filter_text                    .as_deref()                    .and_then(|filter_text| {                        text.find(filter_text)                            .map(|start| start..start + filter_text.len())                    })                    .unwrap_or(0..label.len());                return Some(CodeLabel::new(text, filter_range, runs));            }            Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {                if let Some(signature) = detail.strip_prefix("func") {                    let text = format!("{label}{signature}");                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());                    let runs = adjust_runs(                        name_offset,                        language.highlight_text(&source, 5..5 + text[name_offset..].len()),                    );                    let filter_range = completion                        .filter_text                        .as_deref()                        .and_then(|filter_text| {                            text.find(filter_text)                                .map(|start| start..start + filter_text.len())                        })                        .unwrap_or(0..label.len());                    return Some(CodeLabel::new(text, filter_range, runs));                }            }            _ => {}        }        None    }     async fn label_for_symbol(        &self,        name: &str,        kind: lsp::SymbolKind,        language: &Arc<Language>,    ) -> Option<CodeLabel> {        let (text, filter_range, display_range) = match kind {            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {                let text = format!("func {} () {{}}", name);                let filter_range = 5..5 + name.len();                let display_range = 0..filter_range.end;                (text, filter_range, display_range)            }            lsp::SymbolKind::STRUCT => {                let text = format!("type {} struct {{}}", name);                let filter_range = 5..5 + name.len();                let display_range = 0..text.len();                (text, filter_range, display_range)            }            lsp::SymbolKind::INTERFACE => {                let text = format!("type {} interface {{}}", name);                let filter_range = 5..5 + name.len();                let display_range = 0..text.len();                (text, filter_range, display_range)            }            lsp::SymbolKind::CLASS => {                let text = format!("type {} T", name);                let filter_range = 5..5 + name.len();                let display_range = 0..filter_range.end;                (text, filter_range, display_range)            }            lsp::SymbolKind::CONSTANT => {                let text = format!("const {} = nil", name);                let filter_range = 6..6 + name.len();                let display_range = 0..filter_range.end;                (text, filter_range, display_range)            }            lsp::SymbolKind::VARIABLE => {                let text = format!("var {} = nil", name);                let filter_range = 4..4 + name.len();                let display_range = 0..filter_range.end;                (text, filter_range, display_range)            }            lsp::SymbolKind::MODULE => {                let text = format!("package {}", name);                let filter_range = 8..8 + name.len();                let display_range = 0..filter_range.end;                (text, filter_range, display_range)            }            _ => return None,        };         Some(CodeLabel::new(            text[display_range.clone()].to_string(),            filter_range,            language.highlight_text(&text.as_str().into(), display_range),        ))    }     fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {        static REGEX: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));        Some(REGEX.replace_all(message, "\n\n").to_string())    }} fn parse_version_output(output: &Output) -> Result<&str> {    let version_stdout =        str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;     let version = VERSION_REGEX        .find(version_stdout)        .with_context(|| format!("failed to parse version output '{version_stdout}'"))?        .as_str();     Ok(version)} async fn get_cached_server_binary(container_dir: &Path) -> Option<LanguageServerBinary> {    maybe!(async {        let mut last_binary_path = None;        let mut entries = fs::read_dir(container_dir).await?;        while let Some(entry) = entries.next().await {            let entry = entry?;            if entry.file_type().await?.is_file()                && entry                    .file_name()                    .to_str()                    .is_some_and(|name| name.starts_with("gopls_"))            {                last_binary_path = Some(entry.path());            }        }         let path = last_binary_path.context("no cached binary")?;        anyhow::Ok(LanguageServerBinary {            path,            arguments: server_binary_arguments(),            env: None,        })    })    .await    .log_err()} fn adjust_runs(    delta: usize,    mut runs: Vec<(Range<usize>, HighlightId)>,) -> Vec<(Range<usize>, HighlightId)> {    for (range, _) in &mut runs {        range.start += delta;        range.end += delta;    }    runs} pub(crate) struct GoContextProvider; const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));const GO_SUITE_NAME_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("GO_SUITE_NAME")); impl ContextProvider for GoContextProvider {    fn build_context(        &self,        variables: &TaskVariables,        location: ContextLocation<'_>,        _: Option<HashMap<String, String>>,        _: Arc<dyn LanguageToolchainStore>,        cx: &mut gpui::App,    ) -> Task<Result<TaskVariables>> {        let local_abs_path = location            .file_location            .buffer            .read(cx)            .file()            .and_then(|file| Some(file.as_local()?.abs_path(cx)));         let go_package_variable = local_abs_path            .as_deref()            .and_then(|local_abs_path| local_abs_path.parent())            .map(|buffer_dir| {                // Prefer the relative form `./my-nested-package/is-here` over                // absolute path, because it's more readable in the modal, but                // the absolute path also works.                let package_name = variables                    .get(&VariableName::WorktreeRoot)                    .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())                    .map(|relative_pkg_dir| {                        if relative_pkg_dir.as_os_str().is_empty() {                            ".".into()                        } else {                            format!("./{}", relative_pkg_dir.to_string_lossy())                        }                    })                    .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));                 (GO_PACKAGE_TASK_VARIABLE.clone(), package_name)            }); 

Lines 1–500 of 1345.