Closes #8071 Release Notes: - Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version. Previously we cached gopls (the go language server) as a file called `gopls_{GOPLS_VERSION}`. The go version that gopls was built with is crucial, so we need to cache the go version as well. It's actually super interesting and very clever; gopls uses go to parse the AST and do all the analyzation etc. Go exposes its internals in its standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze the user code. So if there is a new go release that contains new syntax/features/etc. (the libraries `go/parser`, `go/types`, ... change), we can rebuild the same version of `gopls` with the new version of go (with the updated `go/xxx` libraries) to support the new language features. We had some issues around that (e.g., range over integers introduced in go1.22, or custom iterators in go1.23) where we never updated gopls, because we were on the latest gopls version, but built with an old go version. After this PR gopls will be cached under the name `gopls_{GOPLS_VERSION}_go_{GO_VERSION}`. Most users do not see this issue anymore, because after https://github.com/zed-industries/zed/pull/8188 we first check if we can find gopls in the PATH before downloading and caching gopls, but the issue still exists.
715 lines
26 KiB
Rust
715 lines
26 KiB
Rust
use anyhow::{anyhow, Context, Result};
|
|
use async_trait::async_trait;
|
|
use collections::HashMap;
|
|
use futures::StreamExt;
|
|
use gpui::{AppContext, AsyncAppContext, Task};
|
|
use http_client::github::latest_github_release;
|
|
pub use language::*;
|
|
use lsp::{LanguageServerBinary, LanguageServerName};
|
|
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::{
|
|
atomic::{AtomicBool, Ordering::SeqCst},
|
|
Arc, LazyLock,
|
|
},
|
|
};
|
|
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
|
use util::{fs::remove_matching, maybe, ResultExt};
|
|
|
|
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")
|
|
});
|
|
|
|
#[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>,
|
|
_: &AsyncAppContext,
|
|
) -> 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 AsyncAppContext,
|
|
) -> 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(|cx| async move {
|
|
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("gopls");
|
|
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>,
|
|
_: &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::AppContext,
|
|
) -> 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>>,
|
|
_: &AppContext,
|
|
) -> 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 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: ®ex::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)],
|
|
})
|
|
);
|
|
}
|
|
}
|