New list (used tasks are above the separator line, sorted by the usage recency), then all language tasks, then project-local and global tasks are listed. Note that there are two test tasks (for `test_name_1` and `test_name_2` functions) that are created from the same task template: <img width="563" alt="Screenshot 2024-04-10 at 01 00 46" src="https://github.com/zed-industries/zed/assets/2690773/7455a82f-2af2-47bf-99bd-d9c5a36e64ab"> Tasks are deduplicated by labels, with the used tasks left in case of the conflict with the new tasks from the template: <img width="555" alt="Screenshot 2024-04-10 at 01 01 06" src="https://github.com/zed-industries/zed/assets/2690773/8f5a249e-abec-46ef-a991-08c6d0348648"> Regular recent tasks can be now removed too: <img width="565" alt="Screenshot 2024-04-10 at 01 00 55" src="https://github.com/zed-industries/zed/assets/2690773/0976b8fe-b5d7-4d2a-953d-1d8b1f216192"> When the caret is in the place where no function symbol could be retrieved, no cargo tests for function are listed in tasks: <img width="556" alt="image" src="https://github.com/zed-industries/zed/assets/2690773/df30feba-fe27-4645-8be9-02afc70f02da"> Part of https://github.com/zed-industries/zed/issues/10132 Reworks the task code to simplify it and enable proper task labels. * removes `trait Task`, renames `Definition` into `TaskTemplate` and use that instead of `Arc<dyn Task>` everywhere * implement more generic `TaskId` generation that depends on the `TaskContext` and `TaskTemplate` * remove `TaskId` out of the template and only create it after "resolving" the template into the `ResolvedTask`: this way, task templates, task state (`TaskContext`) and task "result" (resolved state) are clearly separated and are not mixed * implement the logic for filtering out non-related language tasks and tasks that have non-resolved Zed task variables * rework Zed template-vs-resolved-task display in modal: now all reruns and recently used tasks are resolved tasks with "fixed" context (unless configured otherwise in the task json) that are always shown, and Zed can add on top tasks with different context that are derived from the same template as the used, resolved tasks * sort the tasks list better, showing more specific and least recently used tasks higher * shows a separator between used and unused tasks, allow removing the used tasks same as the oneshot ones * remote the Oneshot task source as redundant: all oneshot tasks are now stored in the inventory's history * when reusing the tasks as query in the modal, paste the expanded task label now, show trimmed resolved label in the modal * adjusts Rust and Elixir task labels to be more descriptive and closer to bash scripts Release Notes: - Improved task modal ordering, run and deletion capabilities
753 lines
27 KiB
Rust
753 lines
27 KiB
Rust
use anyhow::{anyhow, bail, Context, Result};
|
||
use async_compression::futures::bufread::GzipDecoder;
|
||
use async_trait::async_trait;
|
||
use futures::{io::BufReader, StreamExt};
|
||
use gpui::AsyncAppContext;
|
||
pub use language::*;
|
||
use lazy_static::lazy_static;
|
||
use lsp::LanguageServerBinary;
|
||
use project::project_settings::ProjectSettings;
|
||
use regex::Regex;
|
||
use settings::Settings;
|
||
use smol::fs::{self, File};
|
||
use std::{
|
||
any::Any,
|
||
borrow::Cow,
|
||
env::consts,
|
||
path::{Path, PathBuf},
|
||
sync::Arc,
|
||
};
|
||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||
use util::{
|
||
fs::remove_matching,
|
||
github::{latest_github_release, GitHubLspBinaryVersion},
|
||
maybe, ResultExt,
|
||
};
|
||
|
||
pub struct RustLspAdapter;
|
||
|
||
impl RustLspAdapter {
|
||
const SERVER_NAME: &'static str = "rust-analyzer";
|
||
}
|
||
|
||
#[async_trait(?Send)]
|
||
impl LspAdapter for RustLspAdapter {
|
||
fn name(&self) -> LanguageServerName {
|
||
LanguageServerName(Self::SERVER_NAME.into())
|
||
}
|
||
|
||
async fn check_if_user_installed(
|
||
&self,
|
||
_delegate: &dyn LspAdapterDelegate,
|
||
cx: &AsyncAppContext,
|
||
) -> Option<LanguageServerBinary> {
|
||
let binary = cx
|
||
.update(|cx| {
|
||
ProjectSettings::get_global(cx)
|
||
.lsp
|
||
.get(Self::SERVER_NAME)
|
||
.and_then(|s| s.binary.clone())
|
||
})
|
||
.ok()??;
|
||
|
||
let path = binary.path?;
|
||
Some(LanguageServerBinary {
|
||
path: path.into(),
|
||
arguments: binary
|
||
.arguments
|
||
.unwrap_or_default()
|
||
.iter()
|
||
.map(|arg| arg.into())
|
||
.collect(),
|
||
env: None,
|
||
})
|
||
}
|
||
|
||
async fn fetch_latest_server_version(
|
||
&self,
|
||
delegate: &dyn LspAdapterDelegate,
|
||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||
let release = latest_github_release(
|
||
"rust-lang/rust-analyzer",
|
||
true,
|
||
false,
|
||
delegate.http_client(),
|
||
)
|
||
.await?;
|
||
let os = match consts::OS {
|
||
"macos" => "apple-darwin",
|
||
"linux" => "unknown-linux-gnu",
|
||
"windows" => "pc-windows-msvc",
|
||
other => bail!("Running on unsupported os: {other}"),
|
||
};
|
||
let asset_name = format!("rust-analyzer-{}-{os}.gz", consts::ARCH);
|
||
let asset = release
|
||
.assets
|
||
.iter()
|
||
.find(|asset| asset.name == asset_name)
|
||
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
||
Ok(Box::new(GitHubLspBinaryVersion {
|
||
name: release.tag_name,
|
||
url: asset.browser_download_url.clone(),
|
||
}))
|
||
}
|
||
|
||
async fn fetch_server_binary(
|
||
&self,
|
||
version: Box<dyn 'static + Send + Any>,
|
||
container_dir: PathBuf,
|
||
delegate: &dyn LspAdapterDelegate,
|
||
) -> Result<LanguageServerBinary> {
|
||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||
let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
|
||
|
||
if fs::metadata(&destination_path).await.is_err() {
|
||
let mut response = delegate
|
||
.http_client()
|
||
.get(&version.url, Default::default(), true)
|
||
.await
|
||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||
let mut file = File::create(&destination_path).await?;
|
||
futures::io::copy(decompressed_bytes, &mut file).await?;
|
||
// todo("windows")
|
||
#[cfg(not(windows))]
|
||
{
|
||
fs::set_permissions(
|
||
&destination_path,
|
||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||
)
|
||
.await?;
|
||
}
|
||
|
||
remove_matching(&container_dir, |entry| entry != destination_path).await;
|
||
}
|
||
|
||
Ok(LanguageServerBinary {
|
||
path: destination_path,
|
||
env: None,
|
||
arguments: Default::default(),
|
||
})
|
||
}
|
||
|
||
async fn cached_server_binary(
|
||
&self,
|
||
container_dir: PathBuf,
|
||
_: &dyn LspAdapterDelegate,
|
||
) -> Option<LanguageServerBinary> {
|
||
get_cached_server_binary(container_dir).await
|
||
}
|
||
|
||
async fn installation_test_binary(
|
||
&self,
|
||
container_dir: PathBuf,
|
||
) -> Option<LanguageServerBinary> {
|
||
get_cached_server_binary(container_dir)
|
||
.await
|
||
.map(|mut binary| {
|
||
binary.arguments = vec!["--help".into()];
|
||
binary
|
||
})
|
||
}
|
||
|
||
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
|
||
vec!["rustc".into()]
|
||
}
|
||
|
||
fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
|
||
Some("rust-analyzer/flycheck".into())
|
||
}
|
||
|
||
fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
|
||
lazy_static! {
|
||
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
|
||
}
|
||
|
||
for diagnostic in &mut params.diagnostics {
|
||
for message in diagnostic
|
||
.related_information
|
||
.iter_mut()
|
||
.flatten()
|
||
.map(|info| &mut info.message)
|
||
.chain([&mut diagnostic.message])
|
||
{
|
||
if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
|
||
*message = sanitized;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async fn label_for_completion(
|
||
&self,
|
||
completion: &lsp::CompletionItem,
|
||
language: &Arc<Language>,
|
||
) -> Option<CodeLabel> {
|
||
match completion.kind {
|
||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||
let detail = completion.detail.as_ref().unwrap();
|
||
let name = &completion.label;
|
||
let text = format!("{}: {}", name, detail);
|
||
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
|
||
let runs = language.highlight_text(&source, 11..11 + text.len());
|
||
return Some(CodeLabel {
|
||
text,
|
||
runs,
|
||
filter_range: 0..name.len(),
|
||
});
|
||
}
|
||
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
|
||
if completion.detail.is_some()
|
||
&& completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
|
||
{
|
||
let detail = completion.detail.as_ref().unwrap();
|
||
let name = &completion.label;
|
||
let text = format!("{}: {}", name, detail);
|
||
let source = Rope::from(format!("let {} = ();", text).as_str());
|
||
let runs = language.highlight_text(&source, 4..4 + text.len());
|
||
return Some(CodeLabel {
|
||
text,
|
||
runs,
|
||
filter_range: 0..name.len(),
|
||
});
|
||
}
|
||
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
|
||
if completion.detail.is_some() =>
|
||
{
|
||
lazy_static! {
|
||
static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
|
||
}
|
||
let detail = completion.detail.as_ref().unwrap();
|
||
const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
|
||
let prefix = FUNCTION_PREFIXES
|
||
.iter()
|
||
.find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
|
||
// fn keyword should be followed by opening parenthesis.
|
||
if let Some((prefix, suffix)) = prefix {
|
||
if suffix.starts_with('(') {
|
||
let text = REGEX.replace(&completion.label, suffix).to_string();
|
||
let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
|
||
let run_start = prefix.len() + 1;
|
||
let runs =
|
||
language.highlight_text(&source, run_start..run_start + text.len());
|
||
return Some(CodeLabel {
|
||
filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
|
||
text,
|
||
runs,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
Some(kind) => {
|
||
let highlight_name = match kind {
|
||
lsp::CompletionItemKind::STRUCT
|
||
| lsp::CompletionItemKind::INTERFACE
|
||
| lsp::CompletionItemKind::ENUM => Some("type"),
|
||
lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
|
||
lsp::CompletionItemKind::KEYWORD => Some("keyword"),
|
||
lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
|
||
Some("constant")
|
||
}
|
||
_ => None,
|
||
};
|
||
let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
|
||
let mut label = CodeLabel::plain(completion.label.clone(), None);
|
||
label.runs.push((
|
||
0..label.text.rfind('(').unwrap_or(label.text.len()),
|
||
highlight_id,
|
||
));
|
||
return Some(label);
|
||
}
|
||
_ => {}
|
||
}
|
||
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!("fn {} () {{}}", name);
|
||
let filter_range = 3..3 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::STRUCT => {
|
||
let text = format!("struct {} {{}}", name);
|
||
let filter_range = 7..7 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::ENUM => {
|
||
let text = format!("enum {} {{}}", name);
|
||
let filter_range = 5..5 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::INTERFACE => {
|
||
let text = format!("trait {} {{}}", name);
|
||
let filter_range = 6..6 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::CONSTANT => {
|
||
let text = format!("const {}: () = ();", name);
|
||
let filter_range = 6..6 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::MODULE => {
|
||
let text = format!("mod {} {{}}", name);
|
||
let filter_range = 4..4 + name.len();
|
||
let display_range = 0..filter_range.end;
|
||
(text, filter_range, display_range)
|
||
}
|
||
lsp::SymbolKind::TYPE_PARAMETER => {
|
||
let text = format!("type {} {{}}", name);
|
||
let filter_range = 5..5 + 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,
|
||
})
|
||
}
|
||
}
|
||
|
||
pub(crate) struct RustContextProvider;
|
||
|
||
const RUST_PACKAGE_TASK_VARIABLE: VariableName =
|
||
VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
|
||
|
||
impl ContextProvider for RustContextProvider {
|
||
fn build_context(
|
||
&self,
|
||
location: Location,
|
||
cx: &mut gpui::AppContext,
|
||
) -> Result<TaskVariables> {
|
||
let mut context = SymbolContextProvider.build_context(location.clone(), cx)?;
|
||
|
||
let local_abs_path = location
|
||
.buffer
|
||
.read(cx)
|
||
.file()
|
||
.and_then(|file| Some(file.as_local()?.abs_path(cx)));
|
||
if let Some(package_name) = local_abs_path
|
||
.as_deref()
|
||
.and_then(|local_abs_path| local_abs_path.parent())
|
||
.and_then(human_readable_package_name)
|
||
{
|
||
context.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
|
||
}
|
||
|
||
Ok(context)
|
||
}
|
||
|
||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||
Some(TaskTemplates(vec![
|
||
TaskTemplate {
|
||
label: format!(
|
||
"cargo check -p {}",
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||
),
|
||
command: "cargo".into(),
|
||
args: vec![
|
||
"check".into(),
|
||
"-p".into(),
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||
],
|
||
..TaskTemplate::default()
|
||
},
|
||
TaskTemplate {
|
||
label: "cargo check --workspace --all-targets".into(),
|
||
command: "cargo".into(),
|
||
args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
|
||
..TaskTemplate::default()
|
||
},
|
||
TaskTemplate {
|
||
label: format!(
|
||
"cargo test -p {} {} -- --nocapture",
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||
VariableName::Symbol.template_value(),
|
||
),
|
||
command: "cargo".into(),
|
||
args: vec![
|
||
"test".into(),
|
||
"-p".into(),
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||
VariableName::Symbol.template_value(),
|
||
"--".into(),
|
||
"--nocapture".into(),
|
||
],
|
||
..TaskTemplate::default()
|
||
},
|
||
TaskTemplate {
|
||
label: format!(
|
||
"cargo test -p {}",
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value()
|
||
),
|
||
command: "cargo".into(),
|
||
args: vec![
|
||
"test".into(),
|
||
"-p".into(),
|
||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||
],
|
||
..TaskTemplate::default()
|
||
},
|
||
TaskTemplate {
|
||
label: "cargo run".into(),
|
||
command: "cargo".into(),
|
||
args: vec!["run".into()],
|
||
..TaskTemplate::default()
|
||
},
|
||
TaskTemplate {
|
||
label: "cargo clean".into(),
|
||
command: "cargo".into(),
|
||
args: vec!["clean".into()],
|
||
..TaskTemplate::default()
|
||
},
|
||
]))
|
||
}
|
||
}
|
||
|
||
fn human_readable_package_name(package_directory: &Path) -> Option<String> {
|
||
fn split_off_suffix(input: &str, suffix_start: char) -> &str {
|
||
match input.rsplit_once(suffix_start) {
|
||
Some((without_suffix, _)) => without_suffix,
|
||
None => input,
|
||
}
|
||
}
|
||
|
||
let pkgid = String::from_utf8(
|
||
std::process::Command::new("cargo")
|
||
.current_dir(package_directory)
|
||
.arg("pkgid")
|
||
.output()
|
||
.log_err()?
|
||
.stdout,
|
||
)
|
||
.ok()?;
|
||
// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
|
||
// Output example in the root of Zed project:
|
||
// ```bash
|
||
// ❯ cargo pkgid zed
|
||
// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
|
||
// ```
|
||
// Extrarct the package name from the output according to the spec:
|
||
// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
|
||
let mut package_name = pkgid.trim();
|
||
package_name = split_off_suffix(package_name, '#');
|
||
package_name = split_off_suffix(package_name, '?');
|
||
let (_, package_name) = package_name.rsplit_once('/')?;
|
||
Some(package_name.to_string())
|
||
}
|
||
|
||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||
maybe!(async {
|
||
let mut last = None;
|
||
let mut entries = fs::read_dir(&container_dir).await?;
|
||
while let Some(entry) = entries.next().await {
|
||
last = Some(entry?.path());
|
||
}
|
||
|
||
anyhow::Ok(LanguageServerBinary {
|
||
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
|
||
env: None,
|
||
arguments: Default::default(),
|
||
})
|
||
})
|
||
.await
|
||
.log_err()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use std::num::NonZeroU32;
|
||
|
||
use super::*;
|
||
use crate::language;
|
||
use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
|
||
use language::language_settings::AllLanguageSettings;
|
||
use settings::SettingsStore;
|
||
use theme::SyntaxTheme;
|
||
|
||
#[gpui::test]
|
||
async fn test_process_rust_diagnostics() {
|
||
let mut params = lsp::PublishDiagnosticsParams {
|
||
uri: lsp::Url::from_file_path("/a").unwrap(),
|
||
version: None,
|
||
diagnostics: vec![
|
||
// no newlines
|
||
lsp::Diagnostic {
|
||
message: "use of moved value `a`".to_string(),
|
||
..Default::default()
|
||
},
|
||
// newline at the end of a code span
|
||
lsp::Diagnostic {
|
||
message: "consider importing this struct: `use b::c;\n`".to_string(),
|
||
..Default::default()
|
||
},
|
||
// code span starting right after a newline
|
||
lsp::Diagnostic {
|
||
message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
|
||
.to_string(),
|
||
..Default::default()
|
||
},
|
||
],
|
||
};
|
||
RustLspAdapter.process_diagnostics(&mut params);
|
||
|
||
assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
|
||
|
||
// remove trailing newline from code span
|
||
assert_eq!(
|
||
params.diagnostics[1].message,
|
||
"consider importing this struct: `use b::c;`"
|
||
);
|
||
|
||
// do not remove newline before the start of code span
|
||
assert_eq!(
|
||
params.diagnostics[2].message,
|
||
"cannot borrow `self.d` as mutable\n`self` is a `&` reference"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_rust_label_for_completion() {
|
||
let adapter = Arc::new(RustLspAdapter);
|
||
let language = language("rust", tree_sitter_rust::language());
|
||
let grammar = language.grammar().unwrap();
|
||
let theme = SyntaxTheme::new_test([
|
||
("type", Hsla::default()),
|
||
("keyword", Hsla::default()),
|
||
("function", Hsla::default()),
|
||
("property", Hsla::default()),
|
||
]);
|
||
|
||
language.set_theme(&theme);
|
||
|
||
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_field = grammar.highlight_id_for_name("property").unwrap();
|
||
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_completion(
|
||
&lsp::CompletionItem {
|
||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||
label: "hello(…)".to_string(),
|
||
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
|
||
..Default::default()
|
||
},
|
||
&language
|
||
)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
|
||
filter_range: 0..5,
|
||
runs: vec![
|
||
(0..5, highlight_function),
|
||
(7..10, highlight_keyword),
|
||
(11..17, highlight_type),
|
||
(18..19, highlight_type),
|
||
(25..28, highlight_type),
|
||
(29..30, highlight_type),
|
||
],
|
||
})
|
||
);
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_completion(
|
||
&lsp::CompletionItem {
|
||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||
label: "hello(…)".to_string(),
|
||
detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
|
||
..Default::default()
|
||
},
|
||
&language
|
||
)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
|
||
filter_range: 0..5,
|
||
runs: vec![
|
||
(0..5, highlight_function),
|
||
(7..10, highlight_keyword),
|
||
(11..17, highlight_type),
|
||
(18..19, highlight_type),
|
||
(25..28, highlight_type),
|
||
(29..30, highlight_type),
|
||
],
|
||
})
|
||
);
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_completion(
|
||
&lsp::CompletionItem {
|
||
kind: Some(lsp::CompletionItemKind::FIELD),
|
||
label: "len".to_string(),
|
||
detail: Some("usize".to_string()),
|
||
..Default::default()
|
||
},
|
||
&language
|
||
)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "len: usize".to_string(),
|
||
filter_range: 0..3,
|
||
runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
|
||
})
|
||
);
|
||
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_completion(
|
||
&lsp::CompletionItem {
|
||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||
label: "hello(…)".to_string(),
|
||
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
|
||
..Default::default()
|
||
},
|
||
&language
|
||
)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
|
||
filter_range: 0..5,
|
||
runs: vec![
|
||
(0..5, highlight_function),
|
||
(7..10, highlight_keyword),
|
||
(11..17, highlight_type),
|
||
(18..19, highlight_type),
|
||
(25..28, highlight_type),
|
||
(29..30, highlight_type),
|
||
],
|
||
})
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_rust_label_for_symbol() {
|
||
let adapter = Arc::new(RustLspAdapter);
|
||
let language = language("rust", tree_sitter_rust::language());
|
||
let grammar = language.grammar().unwrap();
|
||
let theme = SyntaxTheme::new_test([
|
||
("type", Hsla::default()),
|
||
("keyword", Hsla::default()),
|
||
("function", Hsla::default()),
|
||
("property", Hsla::default()),
|
||
]);
|
||
|
||
language.set_theme(&theme);
|
||
|
||
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();
|
||
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "fn hello".to_string(),
|
||
filter_range: 3..8,
|
||
runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
|
||
})
|
||
);
|
||
|
||
assert_eq!(
|
||
adapter
|
||
.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
|
||
.await,
|
||
Some(CodeLabel {
|
||
text: "type World".to_string(),
|
||
filter_range: 5..10,
|
||
runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
|
||
})
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_rust_autoindent(cx: &mut TestAppContext) {
|
||
// cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
cx.update(|cx| {
|
||
let test_settings = SettingsStore::test(cx);
|
||
cx.set_global(test_settings);
|
||
language::init(cx);
|
||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||
store.update_user_settings::<AllLanguageSettings>(cx, |s| {
|
||
s.defaults.tab_size = NonZeroU32::new(2);
|
||
});
|
||
});
|
||
});
|
||
|
||
let language = crate::language("rust", tree_sitter_rust::language());
|
||
|
||
cx.new_model(|cx| {
|
||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
||
|
||
// indent between braces
|
||
buffer.set_text("fn a() {}", cx);
|
||
let ix = buffer.len() - 1;
|
||
buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "fn a() {\n \n}");
|
||
|
||
// indent between braces, even after empty lines
|
||
buffer.set_text("fn a() {\n\n\n}", cx);
|
||
let ix = buffer.len() - 2;
|
||
buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "fn a() {\n\n\n \n}");
|
||
|
||
// indent a line that continues a field expression
|
||
buffer.set_text("fn a() {\n \n}", cx);
|
||
let ix = buffer.len() - 2;
|
||
buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
|
||
|
||
// indent further lines that continue the field expression, even after empty lines
|
||
let ix = buffer.len() - 2;
|
||
buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
|
||
|
||
// dedent the line after the field expression
|
||
let ix = buffer.len() - 2;
|
||
buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(
|
||
buffer.text(),
|
||
"fn a() {\n b\n .c\n \n .d;\n e\n}"
|
||
);
|
||
|
||
// indent inside a struct within a call
|
||
buffer.set_text("const a: B = c(D {});", cx);
|
||
let ix = buffer.len() - 3;
|
||
buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "const a: B = c(D {\n \n});");
|
||
|
||
// indent further inside a nested call
|
||
let ix = buffer.len() - 4;
|
||
buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});");
|
||
|
||
// keep that indent after an empty line
|
||
let ix = buffer.len() - 8;
|
||
buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
|
||
assert_eq!(
|
||
buffer.text(),
|
||
"const a: B = c(D {\n e: f(\n \n \n )\n});"
|
||
);
|
||
|
||
buffer
|
||
});
|
||
}
|
||
}
|