ZIm/crates/languages/src/go.rs
Piotr Osiewicz 09d3ff9dbe
debugger: Rework language association with the debuggers (#29945)
- Languages now define their preferred debuggers in `config.toml`.
- `LanguageRegistry` now exposes language config even for languages that
are not yet loaded. This necessitated extension registry changes (we now
deserialize config.toml of all language entries when loading new
extension index), but it should be backwards compatible with the old
format. /cc @maxdeviant

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
2025-05-06 20:16:41 +02:00

741 lines
26 KiB
Rust

use anyhow::{Context as _, Result, anyhow};
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 lsp::{LanguageServerBinary, LanguageServerName};
use project::Fs;
use regex::Regex;
use serde_json::json;
use smol::fs;
use std::{
any::Any,
borrow::Cow,
ffi::{OsStr, OsString},
ops::Range,
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"
};
#[async_trait(?Send)]
impl super::LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
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(Box::new(version) as Box<_>)
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Arc<dyn LanguageToolchainStore>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: server_binary_arguments(),
env: None,
})
}
fn will_fetch_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncApp,
) -> Option<Task<Result<()>>> {
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.";
let delegate = delegate.clone();
Some(cx.spawn(async move |cx| {
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);
})?
}
return Err(anyhow!("cannot install gopls"));
}
Ok(())
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
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)?;
let version = version.downcast::<Option<String>>().unwrap();
let this = *self;
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 {
if 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) = this
.cached_server_binary(container_dir.clone(), delegate)
.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)
);
return Err(anyhow!(
"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 fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
"usePlaceholders": true,
"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.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
});
}
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.len()),
);
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
});
}
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.len()),
);
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
});
}
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.len()),
);
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
});
}
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.len()),
);
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
});
}
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.len()),
);
return Some(CodeLabel {
filter_range: 0..label.len(),
text,
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 {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
filter_range,
})
}
}
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: PathBuf) -> 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()
.map_or(false, |name| name.starts_with("gopls_"))
{
last_binary_path = Some(entry.path());
}
}
if let Some(path) = last_binary_path {
Ok(LanguageServerBinary {
path,
arguments: server_binary_arguments(),
env: None,
})
} else {
Err(anyhow!("no cached binary"))
}
})
.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"));
impl ContextProvider for GoContextProvider {
fn build_context(
&self,
variables: &TaskVariables,
location: &Location,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::App,
) -> Task<Result<TaskVariables>> {
let local_abs_path = 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.to_string())
});
let go_module_root_variable = local_abs_path
.as_deref()
.and_then(|local_abs_path| local_abs_path.parent())
.map(|buffer_dir| {
// Walk dirtree up until getting the first go.mod file
let module_dir = buffer_dir
.ancestors()
.find(|dir| dir.join("go.mod").is_file())
.map(|dir| dir.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
(GO_MODULE_ROOT_TASK_VARIABLE.clone(), module_dir)
});
let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
.map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
Task::ready(Ok(TaskVariables::from_iter(
[
go_package_variable,
go_subtest_variable,
go_module_root_variable,
]
.into_iter()
.flatten(),
)))
}
fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: &App,
) -> Option<TaskTemplates> {
let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
None
} else {
Some("$ZED_DIRNAME".to_string())
};
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
Some(TaskTemplates(vec![
TaskTemplate {
label: format!(
"go test {} -run {}",
GO_PACKAGE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: "go".into(),
args: vec![
"test".into(),
"-run".into(),
format!("\\^{}\\$", VariableName::Symbol.template_value(),),
],
tags: vec!["go-test".to_owned()],
cwd: package_cwd.clone(),
..TaskTemplate::default()
},
TaskTemplate {
label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
command: "go".into(),
args: vec!["test".into()],
cwd: package_cwd.clone(),
..TaskTemplate::default()
},
TaskTemplate {
label: "go test ./...".into(),
command: "go".into(),
args: vec!["test".into(), "./...".into()],
cwd: module_cwd.clone(),
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"go test {} -v -run {}/{}",
GO_PACKAGE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
),
command: "go".into(),
args: vec![
"test".into(),
"-v".into(),
"-run".into(),
format!(
"\\^{}\\$/\\^{}\\$",
VariableName::Symbol.template_value(),
GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
),
],
cwd: package_cwd.clone(),
tags: vec!["go-subtest".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"go test {} -bench {}",
GO_PACKAGE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value()
),
command: "go".into(),
args: vec![
"test".into(),
"-benchmem".into(),
"-run='^$'".into(),
"-bench".into(),
format!("\\^{}\\$", VariableName::Symbol.template_value()),
],
cwd: package_cwd.clone(),
tags: vec!["go-benchmark".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"go test {} -fuzz=Fuzz -run {}",
GO_PACKAGE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: "go".into(),
args: vec![
"test".into(),
"-fuzz=Fuzz".into(),
"-run".into(),
format!("\\^{}\\$", VariableName::Symbol.template_value(),),
],
tags: vec!["go-fuzz".to_owned()],
cwd: package_cwd.clone(),
..TaskTemplate::default()
},
TaskTemplate {
label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
command: "go".into(),
args: vec!["run".into(), ".".into()],
cwd: package_cwd.clone(),
tags: vec!["go-main".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
command: "go".into(),
args: vec!["generate".into()],
cwd: package_cwd.clone(),
tags: vec!["go-generate".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: "go generate ./...".into(),
command: "go".into(),
args: vec!["generate".into(), "./...".into()],
cwd: module_cwd.clone(),
..TaskTemplate::default()
},
]))
}
}
fn extract_subtest_name(input: &str) -> Option<String> {
let replaced_spaces = input.trim_matches('"').replace(' ', "_");
Some(
GO_ESCAPE_SUBTEST_NAME_REGEX
.replace_all(&replaced_spaces, |caps: &regex::Captures| {
format!("\\{}", &caps[0])
})
.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::language;
use gpui::Hsla;
use theme::SyntaxTheme;
#[gpui::test]
async fn test_go_label_for_completion() {
let adapter = Arc::new(GoLspAdapter);
let language = language("go", tree_sitter_go::LANGUAGE.into());
let theme = SyntaxTheme::new_test([
("type", Hsla::default()),
("keyword", Hsla::default()),
("function", Hsla::default()),
("number", Hsla::default()),
("property", Hsla::default()),
]);
language.set_theme(&theme);
let grammar = language.grammar().unwrap();
let highlight_function = grammar.highlight_id_for_name("function").unwrap();
let highlight_type = grammar.highlight_id_for_name("type").unwrap();
let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
let highlight_number = grammar.highlight_id_for_name("number").unwrap();
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "Hello".to_string(),
detail: Some("func(a B) c.D".to_string()),
..Default::default()
},
&language
)
.await,
Some(CodeLabel {
text: "Hello(a B) c.D".to_string(),
filter_range: 0..5,
runs: vec![
(0..5, highlight_function),
(8..9, highlight_type),
(13..14, highlight_type),
],
})
);
// Nested methods
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::METHOD),
label: "one.two.Three".to_string(),
detail: Some("func() [3]interface{}".to_string()),
..Default::default()
},
&language
)
.await,
Some(CodeLabel {
text: "one.two.Three() [3]interface{}".to_string(),
filter_range: 0..13,
runs: vec![
(8..13, highlight_function),
(17..18, highlight_number),
(19..28, highlight_keyword),
],
})
);
// Nested fields
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "two.Three".to_string(),
detail: Some("a.Bcd".to_string()),
..Default::default()
},
&language
)
.await,
Some(CodeLabel {
text: "two.Three a.Bcd".to_string(),
filter_range: 0..9,
runs: vec![(12..15, highlight_type)],
})
);
}
}