rust.rs

  1use anyhow::{anyhow, bail, Context, Result};
  2use async_compression::futures::bufread::GzipDecoder;
  3use async_trait::async_trait;
  4use futures::{io::BufReader, StreamExt};
  5use gpui::AsyncAppContext;
  6use http::github::{latest_github_release, GitHubLspBinaryVersion};
  7pub use language::*;
  8use lazy_static::lazy_static;
  9use lsp::LanguageServerBinary;
 10use project::project_settings::{BinarySettings, ProjectSettings};
 11use regex::Regex;
 12use settings::Settings;
 13use smol::fs::{self, File};
 14use std::{
 15    any::Any,
 16    borrow::Cow,
 17    env::consts,
 18    path::{Path, PathBuf},
 19    sync::Arc,
 20};
 21use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
 22use util::{fs::remove_matching, maybe, ResultExt};
 23
 24pub struct RustLspAdapter;
 25
 26impl RustLspAdapter {
 27    const SERVER_NAME: &'static str = "rust-analyzer";
 28}
 29
 30#[async_trait(?Send)]
 31impl LspAdapter for RustLspAdapter {
 32    fn name(&self) -> LanguageServerName {
 33        LanguageServerName(Self::SERVER_NAME.into())
 34    }
 35
 36    async fn check_if_user_installed(
 37        &self,
 38        delegate: &dyn LspAdapterDelegate,
 39        cx: &AsyncAppContext,
 40    ) -> Option<LanguageServerBinary> {
 41        let configured_binary = cx.update(|cx| {
 42            ProjectSettings::get_global(cx)
 43                .lsp
 44                .get(Self::SERVER_NAME)
 45                .and_then(|s| s.binary.clone())
 46        });
 47
 48        match configured_binary {
 49            Ok(Some(BinarySettings {
 50                path,
 51                arguments,
 52                path_lookup,
 53            })) => {
 54                let (path, env) = match (path, path_lookup) {
 55                    (Some(path), lookup) => {
 56                        if lookup.is_some() {
 57                            log::warn!(
 58                                "Both `path` and `path_lookup` are set, ignoring `path_lookup`"
 59                            );
 60                        }
 61                        (Some(path.into()), None)
 62                    }
 63                    (None, Some(true)) => {
 64                        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
 65                        let env = delegate.shell_env().await;
 66                        (Some(path), Some(env))
 67                    }
 68                    (None, Some(false)) | (None, None) => (None, None),
 69                };
 70                path.map(|path| LanguageServerBinary {
 71                    path,
 72                    arguments: arguments
 73                        .unwrap_or_default()
 74                        .iter()
 75                        .map(|arg| arg.into())
 76                        .collect(),
 77                    env,
 78                })
 79            }
 80            _ => None,
 81        }
 82    }
 83
 84    async fn fetch_latest_server_version(
 85        &self,
 86        delegate: &dyn LspAdapterDelegate,
 87    ) -> Result<Box<dyn 'static + Send + Any>> {
 88        let release = latest_github_release(
 89            "rust-lang/rust-analyzer",
 90            true,
 91            false,
 92            delegate.http_client(),
 93        )
 94        .await?;
 95        let os = match consts::OS {
 96            "macos" => "apple-darwin",
 97            "linux" => "unknown-linux-gnu",
 98            "windows" => "pc-windows-msvc",
 99            other => bail!("Running on unsupported os: {other}"),
100        };
101        let asset_name = format!("rust-analyzer-{}-{os}.gz", consts::ARCH);
102        let asset = release
103            .assets
104            .iter()
105            .find(|asset| asset.name == asset_name)
106            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
107        Ok(Box::new(GitHubLspBinaryVersion {
108            name: release.tag_name,
109            url: asset.browser_download_url.clone(),
110        }))
111    }
112
113    async fn fetch_server_binary(
114        &self,
115        version: Box<dyn 'static + Send + Any>,
116        container_dir: PathBuf,
117        delegate: &dyn LspAdapterDelegate,
118    ) -> Result<LanguageServerBinary> {
119        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
120        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
121
122        if fs::metadata(&destination_path).await.is_err() {
123            let mut response = delegate
124                .http_client()
125                .get(&version.url, Default::default(), true)
126                .await
127                .map_err(|err| anyhow!("error downloading release: {}", err))?;
128            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
129            let mut file = File::create(&destination_path).await?;
130            futures::io::copy(decompressed_bytes, &mut file).await?;
131            // todo("windows")
132            #[cfg(not(windows))]
133            {
134                fs::set_permissions(
135                    &destination_path,
136                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
137                )
138                .await?;
139            }
140
141            remove_matching(&container_dir, |entry| entry != destination_path).await;
142        }
143
144        Ok(LanguageServerBinary {
145            path: destination_path,
146            env: None,
147            arguments: Default::default(),
148        })
149    }
150
151    async fn cached_server_binary(
152        &self,
153        container_dir: PathBuf,
154        _: &dyn LspAdapterDelegate,
155    ) -> Option<LanguageServerBinary> {
156        get_cached_server_binary(container_dir).await
157    }
158
159    async fn installation_test_binary(
160        &self,
161        container_dir: PathBuf,
162    ) -> Option<LanguageServerBinary> {
163        get_cached_server_binary(container_dir)
164            .await
165            .map(|mut binary| {
166                binary.arguments = vec!["--help".into()];
167                binary
168            })
169    }
170
171    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
172        vec!["rustc".into()]
173    }
174
175    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
176        Some("rust-analyzer/flycheck".into())
177    }
178
179    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
180        lazy_static! {
181            static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
182        }
183
184        for diagnostic in &mut params.diagnostics {
185            for message in diagnostic
186                .related_information
187                .iter_mut()
188                .flatten()
189                .map(|info| &mut info.message)
190                .chain([&mut diagnostic.message])
191            {
192                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
193                    *message = sanitized;
194                }
195            }
196        }
197    }
198
199    async fn label_for_completion(
200        &self,
201        completion: &lsp::CompletionItem,
202        language: &Arc<Language>,
203    ) -> Option<CodeLabel> {
204        let detail = completion
205            .detail
206            .as_ref()
207            .or(completion
208                .label_details
209                .as_ref()
210                .and_then(|detail| detail.detail.as_ref()))
211            .map(ToOwned::to_owned);
212        match completion.kind {
213            Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
214                let name = &completion.label;
215                let text = format!("{}: {}", name, detail.unwrap());
216                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
217                let runs = language.highlight_text(&source, 11..11 + text.len());
218                return Some(CodeLabel {
219                    text,
220                    runs,
221                    filter_range: 0..name.len(),
222                });
223            }
224            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
225                if detail.is_some()
226                    && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
227            {
228                let name = &completion.label;
229                let text = format!("{}: {}", name, detail.unwrap());
230                let source = Rope::from(format!("let {} = ();", text).as_str());
231                let runs = language.highlight_text(&source, 4..4 + text.len());
232                return Some(CodeLabel {
233                    text,
234                    runs,
235                    filter_range: 0..name.len(),
236                });
237            }
238            Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
239                if detail.is_some() =>
240            {
241                lazy_static! {
242                    static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
243                }
244                let detail = detail.unwrap();
245                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
246                let prefix = FUNCTION_PREFIXES
247                    .iter()
248                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
249                // fn keyword should be followed by opening parenthesis.
250                if let Some((prefix, suffix)) = prefix {
251                    if suffix.starts_with('(') {
252                        let text = REGEX.replace(&completion.label, suffix).to_string();
253                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
254                        let run_start = prefix.len() + 1;
255                        let runs =
256                            language.highlight_text(&source, run_start..run_start + text.len());
257                        return Some(CodeLabel {
258                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
259                            text,
260                            runs,
261                        });
262                    }
263                }
264            }
265            Some(kind) => {
266                let highlight_name = match kind {
267                    lsp::CompletionItemKind::STRUCT
268                    | lsp::CompletionItemKind::INTERFACE
269                    | lsp::CompletionItemKind::ENUM => Some("type"),
270                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
271                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
272                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
273                        Some("constant")
274                    }
275                    _ => None,
276                };
277                let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
278                let mut label = completion.label.clone();
279                if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
280                    use std::fmt::Write;
281                    write!(label, "{detail}").ok()?;
282                }
283                let mut label = CodeLabel::plain(label, None);
284                label.runs.push((
285                    0..label.text.rfind('(').unwrap_or(completion.label.len()),
286                    highlight_id,
287                ));
288                return Some(label);
289            }
290            _ => {}
291        }
292        None
293    }
294
295    async fn label_for_symbol(
296        &self,
297        name: &str,
298        kind: lsp::SymbolKind,
299        language: &Arc<Language>,
300    ) -> Option<CodeLabel> {
301        let (text, filter_range, display_range) = match kind {
302            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
303                let text = format!("fn {} () {{}}", name);
304                let filter_range = 3..3 + name.len();
305                let display_range = 0..filter_range.end;
306                (text, filter_range, display_range)
307            }
308            lsp::SymbolKind::STRUCT => {
309                let text = format!("struct {} {{}}", name);
310                let filter_range = 7..7 + name.len();
311                let display_range = 0..filter_range.end;
312                (text, filter_range, display_range)
313            }
314            lsp::SymbolKind::ENUM => {
315                let text = format!("enum {} {{}}", name);
316                let filter_range = 5..5 + name.len();
317                let display_range = 0..filter_range.end;
318                (text, filter_range, display_range)
319            }
320            lsp::SymbolKind::INTERFACE => {
321                let text = format!("trait {} {{}}", name);
322                let filter_range = 6..6 + name.len();
323                let display_range = 0..filter_range.end;
324                (text, filter_range, display_range)
325            }
326            lsp::SymbolKind::CONSTANT => {
327                let text = format!("const {}: () = ();", name);
328                let filter_range = 6..6 + name.len();
329                let display_range = 0..filter_range.end;
330                (text, filter_range, display_range)
331            }
332            lsp::SymbolKind::MODULE => {
333                let text = format!("mod {} {{}}", name);
334                let filter_range = 4..4 + name.len();
335                let display_range = 0..filter_range.end;
336                (text, filter_range, display_range)
337            }
338            lsp::SymbolKind::TYPE_PARAMETER => {
339                let text = format!("type {} {{}}", name);
340                let filter_range = 5..5 + name.len();
341                let display_range = 0..filter_range.end;
342                (text, filter_range, display_range)
343            }
344            _ => return None,
345        };
346
347        Some(CodeLabel {
348            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
349            text: text[display_range].to_string(),
350            filter_range,
351        })
352    }
353}
354
355pub(crate) struct RustContextProvider;
356
357const RUST_PACKAGE_TASK_VARIABLE: VariableName =
358    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
359
360/// The bin name corresponding to the current file in Cargo.toml
361const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
362    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
363
364const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName =
365    VariableName::Custom(Cow::Borrowed("_rust_main_function_end"));
366
367impl ContextProvider for RustContextProvider {
368    fn build_context(
369        &self,
370        task_variables: &TaskVariables,
371        location: &Location,
372        cx: &mut gpui::AppContext,
373    ) -> Result<TaskVariables> {
374        let local_abs_path = location
375            .buffer
376            .read(cx)
377            .file()
378            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
379
380        let local_abs_path = local_abs_path.as_deref();
381
382        let is_main_function = task_variables
383            .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
384            .is_some();
385
386        if is_main_function {
387            if let Some((package_name, bin_name)) = local_abs_path
388                .and_then(|local_abs_path| package_name_and_bin_name_from_abs_path(local_abs_path))
389            {
390                return Ok(TaskVariables::from_iter([
391                    (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
392                    (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
393                ]));
394            }
395        }
396
397        if let Some(package_name) = local_abs_path
398            .and_then(|local_abs_path| local_abs_path.parent())
399            .and_then(human_readable_package_name)
400        {
401            return Ok(TaskVariables::from_iter([(
402                RUST_PACKAGE_TASK_VARIABLE.clone(),
403                package_name,
404            )]));
405        }
406
407        Ok(TaskVariables::default())
408    }
409
410    fn associated_tasks(&self) -> Option<TaskTemplates> {
411        Some(TaskTemplates(vec![
412            TaskTemplate {
413                label: format!(
414                    "cargo check -p {}",
415                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
416                ),
417                command: "cargo".into(),
418                args: vec![
419                    "check".into(),
420                    "-p".into(),
421                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
422                ],
423                cwd: Some("$ZED_DIRNAME".to_owned()),
424                ..TaskTemplate::default()
425            },
426            TaskTemplate {
427                label: "cargo check --workspace --all-targets".into(),
428                command: "cargo".into(),
429                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
430                cwd: Some("$ZED_DIRNAME".to_owned()),
431                ..TaskTemplate::default()
432            },
433            TaskTemplate {
434                label: format!(
435                    "cargo test -p {} {} -- --nocapture",
436                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
437                    VariableName::Symbol.template_value(),
438                ),
439                command: "cargo".into(),
440                args: vec![
441                    "test".into(),
442                    "-p".into(),
443                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
444                    VariableName::Symbol.template_value(),
445                    "--".into(),
446                    "--nocapture".into(),
447                ],
448                tags: vec!["rust-test".to_owned()],
449                cwd: Some("$ZED_DIRNAME".to_owned()),
450                ..TaskTemplate::default()
451            },
452            TaskTemplate {
453                label: format!(
454                    "cargo test -p {} {}",
455                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
456                    VariableName::Stem.template_value(),
457                ),
458                command: "cargo".into(),
459                args: vec![
460                    "test".into(),
461                    "-p".into(),
462                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
463                    VariableName::Stem.template_value(),
464                ],
465                tags: vec!["rust-mod-test".to_owned()],
466                cwd: Some("$ZED_DIRNAME".to_owned()),
467                ..TaskTemplate::default()
468            },
469            TaskTemplate {
470                label: format!(
471                    "cargo run -p {} --bin {}",
472                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
473                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
474                ),
475                command: "cargo".into(),
476                args: vec![
477                    "run".into(),
478                    "-p".into(),
479                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
480                    "--bin".into(),
481                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
482                ],
483                cwd: Some("$ZED_DIRNAME".to_owned()),
484                tags: vec!["rust-main".to_owned()],
485                ..TaskTemplate::default()
486            },
487            TaskTemplate {
488                label: format!(
489                    "cargo test -p {}",
490                    RUST_PACKAGE_TASK_VARIABLE.template_value()
491                ),
492                command: "cargo".into(),
493                args: vec![
494                    "test".into(),
495                    "-p".into(),
496                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
497                ],
498                cwd: Some("$ZED_DIRNAME".to_owned()),
499                ..TaskTemplate::default()
500            },
501            TaskTemplate {
502                label: "cargo run".into(),
503                command: "cargo".into(),
504                args: vec!["run".into()],
505                cwd: Some("$ZED_DIRNAME".to_owned()),
506                ..TaskTemplate::default()
507            },
508            TaskTemplate {
509                label: "cargo clean".into(),
510                command: "cargo".into(),
511                args: vec!["clean".into()],
512                cwd: Some("$ZED_DIRNAME".to_owned()),
513                ..TaskTemplate::default()
514            },
515        ]))
516    }
517}
518
519/// Part of the data structure of Cargo metadata
520#[derive(serde::Deserialize)]
521struct CargoMetadata {
522    packages: Vec<CargoPackage>,
523}
524
525#[derive(serde::Deserialize)]
526struct CargoPackage {
527    id: String,
528    targets: Vec<CargoTarget>,
529}
530
531#[derive(serde::Deserialize)]
532struct CargoTarget {
533    name: String,
534    kind: Vec<String>,
535    src_path: String,
536}
537
538fn package_name_and_bin_name_from_abs_path(abs_path: &Path) -> Option<(String, String)> {
539    let output = std::process::Command::new("cargo")
540        .current_dir(abs_path.parent()?)
541        .arg("metadata")
542        .arg("--no-deps")
543        .arg("--format-version")
544        .arg("1")
545        .output()
546        .log_err()?
547        .stdout;
548
549    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
550
551    retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
552        |(package_id, bin_name)| {
553            let package_name = package_name_from_pkgid(&package_id);
554
555            package_name.map(|package_name| (package_name.to_owned(), bin_name))
556        },
557    )
558}
559
560fn retrieve_package_id_and_bin_name_from_metadata(
561    metadata: CargoMetadata,
562    abs_path: &Path,
563) -> Option<(String, String)> {
564    let abs_path = abs_path.to_str()?;
565
566    for package in metadata.packages {
567        for target in package.targets {
568            let is_bin = target.kind.iter().any(|kind| kind == "bin");
569            if target.src_path == abs_path && is_bin {
570                return Some((package.id, target.name));
571            }
572        }
573    }
574
575    None
576}
577
578fn human_readable_package_name(package_directory: &Path) -> Option<String> {
579    let pkgid = String::from_utf8(
580        std::process::Command::new("cargo")
581            .current_dir(package_directory)
582            .arg("pkgid")
583            .output()
584            .log_err()?
585            .stdout,
586    )
587    .ok()?;
588    Some(package_name_from_pkgid(&pkgid)?.to_owned())
589}
590
591// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
592// Output example in the root of Zed project:
593// ```bash
594// ❯ cargo pkgid zed
595// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
596// ```
597// Another variant, if a project has a custom package name or hyphen in the name:
598// ```
599// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
600// ```
601//
602// Extracts the package name from the output according to the spec:
603// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
604fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
605    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
606        match input.rsplit_once(suffix_start) {
607            Some((without_suffix, _)) => without_suffix,
608            None => input,
609        }
610    }
611
612    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
613    let package_name = match version_suffix.rsplit_once('@') {
614        Some((custom_package_name, _version)) => custom_package_name,
615        None => {
616            let host_and_path = split_off_suffix(version_prefix, '?');
617            let (_, package_name) = host_and_path.rsplit_once('/')?;
618            package_name
619        }
620    };
621    Some(package_name)
622}
623
624async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
625    maybe!(async {
626        let mut last = None;
627        let mut entries = fs::read_dir(&container_dir).await?;
628        while let Some(entry) = entries.next().await {
629            last = Some(entry?.path());
630        }
631
632        anyhow::Ok(LanguageServerBinary {
633            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
634            env: None,
635            arguments: Default::default(),
636        })
637    })
638    .await
639    .log_err()
640}
641
642#[cfg(test)]
643mod tests {
644    use std::num::NonZeroU32;
645
646    use super::*;
647    use crate::language;
648    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
649    use language::language_settings::AllLanguageSettings;
650    use settings::SettingsStore;
651    use theme::SyntaxTheme;
652
653    #[gpui::test]
654    async fn test_process_rust_diagnostics() {
655        let mut params = lsp::PublishDiagnosticsParams {
656            uri: lsp::Url::from_file_path("/a").unwrap(),
657            version: None,
658            diagnostics: vec![
659                // no newlines
660                lsp::Diagnostic {
661                    message: "use of moved value `a`".to_string(),
662                    ..Default::default()
663                },
664                // newline at the end of a code span
665                lsp::Diagnostic {
666                    message: "consider importing this struct: `use b::c;\n`".to_string(),
667                    ..Default::default()
668                },
669                // code span starting right after a newline
670                lsp::Diagnostic {
671                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
672                        .to_string(),
673                    ..Default::default()
674                },
675            ],
676        };
677        RustLspAdapter.process_diagnostics(&mut params);
678
679        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
680
681        // remove trailing newline from code span
682        assert_eq!(
683            params.diagnostics[1].message,
684            "consider importing this struct: `use b::c;`"
685        );
686
687        // do not remove newline before the start of code span
688        assert_eq!(
689            params.diagnostics[2].message,
690            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
691        );
692    }
693
694    #[gpui::test]
695    async fn test_rust_label_for_completion() {
696        let adapter = Arc::new(RustLspAdapter);
697        let language = language("rust", tree_sitter_rust::language());
698        let grammar = language.grammar().unwrap();
699        let theme = SyntaxTheme::new_test([
700            ("type", Hsla::default()),
701            ("keyword", Hsla::default()),
702            ("function", Hsla::default()),
703            ("property", Hsla::default()),
704        ]);
705
706        language.set_theme(&theme);
707
708        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
709        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
710        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
711        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
712
713        assert_eq!(
714            adapter
715                .label_for_completion(
716                    &lsp::CompletionItem {
717                        kind: Some(lsp::CompletionItemKind::FUNCTION),
718                        label: "hello(…)".to_string(),
719                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
720                        ..Default::default()
721                    },
722                    &language
723                )
724                .await,
725            Some(CodeLabel {
726                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
727                filter_range: 0..5,
728                runs: vec![
729                    (0..5, highlight_function),
730                    (7..10, highlight_keyword),
731                    (11..17, highlight_type),
732                    (18..19, highlight_type),
733                    (25..28, highlight_type),
734                    (29..30, highlight_type),
735                ],
736            })
737        );
738        assert_eq!(
739            adapter
740                .label_for_completion(
741                    &lsp::CompletionItem {
742                        kind: Some(lsp::CompletionItemKind::FUNCTION),
743                        label: "hello(…)".to_string(),
744                        detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
745                        ..Default::default()
746                    },
747                    &language
748                )
749                .await,
750            Some(CodeLabel {
751                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
752                filter_range: 0..5,
753                runs: vec![
754                    (0..5, highlight_function),
755                    (7..10, highlight_keyword),
756                    (11..17, highlight_type),
757                    (18..19, highlight_type),
758                    (25..28, highlight_type),
759                    (29..30, highlight_type),
760                ],
761            })
762        );
763        assert_eq!(
764            adapter
765                .label_for_completion(
766                    &lsp::CompletionItem {
767                        kind: Some(lsp::CompletionItemKind::FIELD),
768                        label: "len".to_string(),
769                        detail: Some("usize".to_string()),
770                        ..Default::default()
771                    },
772                    &language
773                )
774                .await,
775            Some(CodeLabel {
776                text: "len: usize".to_string(),
777                filter_range: 0..3,
778                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
779            })
780        );
781
782        assert_eq!(
783            adapter
784                .label_for_completion(
785                    &lsp::CompletionItem {
786                        kind: Some(lsp::CompletionItemKind::FUNCTION),
787                        label: "hello(…)".to_string(),
788                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
789                        ..Default::default()
790                    },
791                    &language
792                )
793                .await,
794            Some(CodeLabel {
795                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
796                filter_range: 0..5,
797                runs: vec![
798                    (0..5, highlight_function),
799                    (7..10, highlight_keyword),
800                    (11..17, highlight_type),
801                    (18..19, highlight_type),
802                    (25..28, highlight_type),
803                    (29..30, highlight_type),
804                ],
805            })
806        );
807    }
808
809    #[gpui::test]
810    async fn test_rust_label_for_symbol() {
811        let adapter = Arc::new(RustLspAdapter);
812        let language = language("rust", tree_sitter_rust::language());
813        let grammar = language.grammar().unwrap();
814        let theme = SyntaxTheme::new_test([
815            ("type", Hsla::default()),
816            ("keyword", Hsla::default()),
817            ("function", Hsla::default()),
818            ("property", Hsla::default()),
819        ]);
820
821        language.set_theme(&theme);
822
823        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
824        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
825        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
826
827        assert_eq!(
828            adapter
829                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
830                .await,
831            Some(CodeLabel {
832                text: "fn hello".to_string(),
833                filter_range: 3..8,
834                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
835            })
836        );
837
838        assert_eq!(
839            adapter
840                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
841                .await,
842            Some(CodeLabel {
843                text: "type World".to_string(),
844                filter_range: 5..10,
845                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
846            })
847        );
848    }
849
850    #[gpui::test]
851    async fn test_rust_autoindent(cx: &mut TestAppContext) {
852        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
853        cx.update(|cx| {
854            let test_settings = SettingsStore::test(cx);
855            cx.set_global(test_settings);
856            language::init(cx);
857            cx.update_global::<SettingsStore, _>(|store, cx| {
858                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
859                    s.defaults.tab_size = NonZeroU32::new(2);
860                });
861            });
862        });
863
864        let language = crate::language("rust", tree_sitter_rust::language());
865
866        cx.new_model(|cx| {
867            let mut buffer = Buffer::local("", cx).with_language(language, cx);
868
869            // indent between braces
870            buffer.set_text("fn a() {}", cx);
871            let ix = buffer.len() - 1;
872            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
873            assert_eq!(buffer.text(), "fn a() {\n  \n}");
874
875            // indent between braces, even after empty lines
876            buffer.set_text("fn a() {\n\n\n}", cx);
877            let ix = buffer.len() - 2;
878            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
879            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
880
881            // indent a line that continues a field expression
882            buffer.set_text("fn a() {\n  \n}", cx);
883            let ix = buffer.len() - 2;
884            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
885            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
886
887            // indent further lines that continue the field expression, even after empty lines
888            let ix = buffer.len() - 2;
889            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
890            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
891
892            // dedent the line after the field expression
893            let ix = buffer.len() - 2;
894            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
895            assert_eq!(
896                buffer.text(),
897                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
898            );
899
900            // indent inside a struct within a call
901            buffer.set_text("const a: B = c(D {});", cx);
902            let ix = buffer.len() - 3;
903            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
904            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
905
906            // indent further inside a nested call
907            let ix = buffer.len() - 4;
908            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
909            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
910
911            // keep that indent after an empty line
912            let ix = buffer.len() - 8;
913            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
914            assert_eq!(
915                buffer.text(),
916                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
917            );
918
919            buffer
920        });
921    }
922
923    #[test]
924    fn test_package_name_from_pkgid() {
925        for (input, expected) in [
926            (
927                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
928                "zed",
929            ),
930            (
931                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
932                "my-custom-package",
933            ),
934        ] {
935            assert_eq!(package_name_from_pkgid(input), Some(expected));
936        }
937    }
938
939    #[test]
940    fn test_retrieve_package_id_and_bin_name_from_metadata() {
941        for (input, absolute_path, expected) in [
942            (
943                r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
944                "/path/to/zed/src/main.rs",
945                Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
946            ),
947            (
948                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
949                "/path/to/custom-package/src/main.rs",
950                Some((
951                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
952                    "my-custom-bin",
953                )),
954            ),
955            (
956                r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-package","kind":["lib"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
957                "/path/to/custom-package/src/main.rs",
958                None,
959            ),
960        ] {
961            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
962
963            let absolute_path = Path::new(absolute_path);
964
965            assert_eq!(
966                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
967                expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
968            );
969        }
970    }
971}