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        match completion.kind {
205            Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
206                let detail = completion.detail.as_ref().unwrap();
207                let name = &completion.label;
208                let text = format!("{}: {}", name, detail);
209                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
210                let runs = language.highlight_text(&source, 11..11 + text.len());
211                return Some(CodeLabel {
212                    text,
213                    runs,
214                    filter_range: 0..name.len(),
215                });
216            }
217            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
218                if completion.detail.is_some()
219                    && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
220            {
221                let detail = completion.detail.as_ref().unwrap();
222                let name = &completion.label;
223                let text = format!("{}: {}", name, detail);
224                let source = Rope::from(format!("let {} = ();", text).as_str());
225                let runs = language.highlight_text(&source, 4..4 + text.len());
226                return Some(CodeLabel {
227                    text,
228                    runs,
229                    filter_range: 0..name.len(),
230                });
231            }
232            Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
233                if completion.detail.is_some() =>
234            {
235                lazy_static! {
236                    static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
237                }
238                let detail = completion.detail.as_ref().unwrap();
239                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
240                let prefix = FUNCTION_PREFIXES
241                    .iter()
242                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
243                // fn keyword should be followed by opening parenthesis.
244                if let Some((prefix, suffix)) = prefix {
245                    if suffix.starts_with('(') {
246                        let text = REGEX.replace(&completion.label, suffix).to_string();
247                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
248                        let run_start = prefix.len() + 1;
249                        let runs =
250                            language.highlight_text(&source, run_start..run_start + text.len());
251                        return Some(CodeLabel {
252                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
253                            text,
254                            runs,
255                        });
256                    }
257                }
258            }
259            Some(kind) => {
260                let highlight_name = match kind {
261                    lsp::CompletionItemKind::STRUCT
262                    | lsp::CompletionItemKind::INTERFACE
263                    | lsp::CompletionItemKind::ENUM => Some("type"),
264                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
265                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
266                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
267                        Some("constant")
268                    }
269                    _ => None,
270                };
271                let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
272                let mut label = CodeLabel::plain(completion.label.clone(), None);
273                label.runs.push((
274                    0..label.text.rfind('(').unwrap_or(label.text.len()),
275                    highlight_id,
276                ));
277                return Some(label);
278            }
279            _ => {}
280        }
281        None
282    }
283
284    async fn label_for_symbol(
285        &self,
286        name: &str,
287        kind: lsp::SymbolKind,
288        language: &Arc<Language>,
289    ) -> Option<CodeLabel> {
290        let (text, filter_range, display_range) = match kind {
291            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
292                let text = format!("fn {} () {{}}", name);
293                let filter_range = 3..3 + name.len();
294                let display_range = 0..filter_range.end;
295                (text, filter_range, display_range)
296            }
297            lsp::SymbolKind::STRUCT => {
298                let text = format!("struct {} {{}}", name);
299                let filter_range = 7..7 + name.len();
300                let display_range = 0..filter_range.end;
301                (text, filter_range, display_range)
302            }
303            lsp::SymbolKind::ENUM => {
304                let text = format!("enum {} {{}}", name);
305                let filter_range = 5..5 + name.len();
306                let display_range = 0..filter_range.end;
307                (text, filter_range, display_range)
308            }
309            lsp::SymbolKind::INTERFACE => {
310                let text = format!("trait {} {{}}", name);
311                let filter_range = 6..6 + name.len();
312                let display_range = 0..filter_range.end;
313                (text, filter_range, display_range)
314            }
315            lsp::SymbolKind::CONSTANT => {
316                let text = format!("const {}: () = ();", name);
317                let filter_range = 6..6 + name.len();
318                let display_range = 0..filter_range.end;
319                (text, filter_range, display_range)
320            }
321            lsp::SymbolKind::MODULE => {
322                let text = format!("mod {} {{}}", name);
323                let filter_range = 4..4 + name.len();
324                let display_range = 0..filter_range.end;
325                (text, filter_range, display_range)
326            }
327            lsp::SymbolKind::TYPE_PARAMETER => {
328                let text = format!("type {} {{}}", name);
329                let filter_range = 5..5 + name.len();
330                let display_range = 0..filter_range.end;
331                (text, filter_range, display_range)
332            }
333            _ => return None,
334        };
335
336        Some(CodeLabel {
337            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
338            text: text[display_range].to_string(),
339            filter_range,
340        })
341    }
342}
343
344pub(crate) struct RustContextProvider;
345
346const RUST_PACKAGE_TASK_VARIABLE: VariableName =
347    VariableName::Custom(Cow::Borrowed("RUST_PACKAGE"));
348
349/// The bin name corresponding to the current file in Cargo.toml
350const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
351    VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME"));
352
353const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName =
354    VariableName::Custom(Cow::Borrowed("_rust_main_function_end"));
355
356impl ContextProvider for RustContextProvider {
357    fn build_context(
358        &self,
359        task_variables: &TaskVariables,
360        location: &Location,
361        cx: &mut gpui::AppContext,
362    ) -> Result<TaskVariables> {
363        let local_abs_path = location
364            .buffer
365            .read(cx)
366            .file()
367            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
368
369        let local_abs_path = local_abs_path.as_deref();
370
371        let is_main_function = task_variables
372            .get(&RUST_MAIN_FUNCTION_TASK_VARIABLE)
373            .is_some();
374
375        if is_main_function {
376            if let Some((package_name, bin_name)) = local_abs_path
377                .and_then(|local_abs_path| package_name_and_bin_name_from_abs_path(local_abs_path))
378            {
379                return Ok(TaskVariables::from_iter([
380                    (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
381                    (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
382                ]));
383            }
384        }
385
386        if let Some(package_name) = local_abs_path
387            .and_then(|local_abs_path| local_abs_path.parent())
388            .and_then(human_readable_package_name)
389        {
390            return Ok(TaskVariables::from_iter([(
391                RUST_PACKAGE_TASK_VARIABLE.clone(),
392                package_name,
393            )]));
394        }
395
396        Ok(TaskVariables::default())
397    }
398
399    fn associated_tasks(&self) -> Option<TaskTemplates> {
400        Some(TaskTemplates(vec![
401            TaskTemplate {
402                label: format!(
403                    "cargo check -p {}",
404                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
405                ),
406                command: "cargo".into(),
407                args: vec![
408                    "check".into(),
409                    "-p".into(),
410                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
411                ],
412                ..TaskTemplate::default()
413            },
414            TaskTemplate {
415                label: "cargo check --workspace --all-targets".into(),
416                command: "cargo".into(),
417                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
418                ..TaskTemplate::default()
419            },
420            TaskTemplate {
421                label: format!(
422                    "cargo test -p {} {} -- --nocapture",
423                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
424                    VariableName::Symbol.template_value(),
425                ),
426                command: "cargo".into(),
427                args: vec![
428                    "test".into(),
429                    "-p".into(),
430                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
431                    VariableName::Symbol.template_value(),
432                    "--".into(),
433                    "--nocapture".into(),
434                ],
435                tags: vec!["rust-test".to_owned()],
436                ..TaskTemplate::default()
437            },
438            TaskTemplate {
439                label: format!(
440                    "cargo test -p {} {}",
441                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
442                    VariableName::Stem.template_value(),
443                ),
444                command: "cargo".into(),
445                args: vec![
446                    "test".into(),
447                    "-p".into(),
448                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
449                    VariableName::Stem.template_value(),
450                ],
451                tags: vec!["rust-mod-test".to_owned()],
452                ..TaskTemplate::default()
453            },
454            TaskTemplate {
455                label: format!(
456                    "cargo run -p {} --bin {}",
457                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
458                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
459                ),
460                command: "cargo".into(),
461                args: vec![
462                    "run".into(),
463                    "-p".into(),
464                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
465                    "--bin".into(),
466                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
467                ],
468                tags: vec!["rust-main".to_owned()],
469                ..TaskTemplate::default()
470            },
471            TaskTemplate {
472                label: format!(
473                    "cargo test -p {}",
474                    RUST_PACKAGE_TASK_VARIABLE.template_value()
475                ),
476                command: "cargo".into(),
477                args: vec![
478                    "test".into(),
479                    "-p".into(),
480                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
481                ],
482                ..TaskTemplate::default()
483            },
484            TaskTemplate {
485                label: "cargo run".into(),
486                command: "cargo".into(),
487                args: vec!["run".into()],
488                ..TaskTemplate::default()
489            },
490            TaskTemplate {
491                label: "cargo clean".into(),
492                command: "cargo".into(),
493                args: vec!["clean".into()],
494                ..TaskTemplate::default()
495            },
496        ]))
497    }
498}
499
500/// Part of the data structure of Cargo metadata
501#[derive(serde::Deserialize)]
502struct CargoMetadata {
503    packages: Vec<CargoPackage>,
504}
505
506#[derive(serde::Deserialize)]
507struct CargoPackage {
508    id: String,
509    targets: Vec<CargoTarget>,
510}
511
512#[derive(serde::Deserialize)]
513struct CargoTarget {
514    name: String,
515    kind: Vec<String>,
516    src_path: String,
517}
518
519fn package_name_and_bin_name_from_abs_path(abs_path: &Path) -> Option<(String, String)> {
520    let output = std::process::Command::new("cargo")
521        .current_dir(abs_path.parent()?)
522        .arg("metadata")
523        .arg("--no-deps")
524        .arg("--format-version")
525        .arg("1")
526        .output()
527        .log_err()?
528        .stdout;
529
530    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
531
532    retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
533        |(package_id, bin_name)| {
534            let package_name = package_name_from_pkgid(&package_id);
535
536            package_name.map(|package_name| (package_name.to_owned(), bin_name))
537        },
538    )
539}
540
541fn retrieve_package_id_and_bin_name_from_metadata(
542    metadata: CargoMetadata,
543    abs_path: &Path,
544) -> Option<(String, String)> {
545    let abs_path = abs_path.to_str()?;
546
547    for package in metadata.packages {
548        for target in package.targets {
549            let is_bin = target.kind.iter().any(|kind| kind == "bin");
550            if target.src_path == abs_path && is_bin {
551                return Some((package.id, target.name));
552            }
553        }
554    }
555
556    None
557}
558
559fn human_readable_package_name(package_directory: &Path) -> Option<String> {
560    let pkgid = String::from_utf8(
561        std::process::Command::new("cargo")
562            .current_dir(package_directory)
563            .arg("pkgid")
564            .output()
565            .log_err()?
566            .stdout,
567    )
568    .ok()?;
569    Some(package_name_from_pkgid(&pkgid)?.to_owned())
570}
571
572// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
573// Output example in the root of Zed project:
574// ```bash
575// ❯ cargo pkgid zed
576// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
577// ```
578// Another variant, if a project has a custom package name or hyphen in the name:
579// ```
580// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
581// ```
582//
583// Extracts the package name from the output according to the spec:
584// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
585fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
586    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
587        match input.rsplit_once(suffix_start) {
588            Some((without_suffix, _)) => without_suffix,
589            None => input,
590        }
591    }
592
593    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
594    let package_name = match version_suffix.rsplit_once('@') {
595        Some((custom_package_name, _version)) => custom_package_name,
596        None => {
597            let host_and_path = split_off_suffix(version_prefix, '?');
598            let (_, package_name) = host_and_path.rsplit_once('/')?;
599            package_name
600        }
601    };
602    Some(package_name)
603}
604
605async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
606    maybe!(async {
607        let mut last = None;
608        let mut entries = fs::read_dir(&container_dir).await?;
609        while let Some(entry) = entries.next().await {
610            last = Some(entry?.path());
611        }
612
613        anyhow::Ok(LanguageServerBinary {
614            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
615            env: None,
616            arguments: Default::default(),
617        })
618    })
619    .await
620    .log_err()
621}
622
623#[cfg(test)]
624mod tests {
625    use std::num::NonZeroU32;
626
627    use super::*;
628    use crate::language;
629    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
630    use language::language_settings::AllLanguageSettings;
631    use settings::SettingsStore;
632    use theme::SyntaxTheme;
633
634    #[gpui::test]
635    async fn test_process_rust_diagnostics() {
636        let mut params = lsp::PublishDiagnosticsParams {
637            uri: lsp::Url::from_file_path("/a").unwrap(),
638            version: None,
639            diagnostics: vec![
640                // no newlines
641                lsp::Diagnostic {
642                    message: "use of moved value `a`".to_string(),
643                    ..Default::default()
644                },
645                // newline at the end of a code span
646                lsp::Diagnostic {
647                    message: "consider importing this struct: `use b::c;\n`".to_string(),
648                    ..Default::default()
649                },
650                // code span starting right after a newline
651                lsp::Diagnostic {
652                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
653                        .to_string(),
654                    ..Default::default()
655                },
656            ],
657        };
658        RustLspAdapter.process_diagnostics(&mut params);
659
660        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
661
662        // remove trailing newline from code span
663        assert_eq!(
664            params.diagnostics[1].message,
665            "consider importing this struct: `use b::c;`"
666        );
667
668        // do not remove newline before the start of code span
669        assert_eq!(
670            params.diagnostics[2].message,
671            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
672        );
673    }
674
675    #[gpui::test]
676    async fn test_rust_label_for_completion() {
677        let adapter = Arc::new(RustLspAdapter);
678        let language = language("rust", tree_sitter_rust::language());
679        let grammar = language.grammar().unwrap();
680        let theme = SyntaxTheme::new_test([
681            ("type", Hsla::default()),
682            ("keyword", Hsla::default()),
683            ("function", Hsla::default()),
684            ("property", Hsla::default()),
685        ]);
686
687        language.set_theme(&theme);
688
689        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
690        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
691        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
692        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
693
694        assert_eq!(
695            adapter
696                .label_for_completion(
697                    &lsp::CompletionItem {
698                        kind: Some(lsp::CompletionItemKind::FUNCTION),
699                        label: "hello(…)".to_string(),
700                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
701                        ..Default::default()
702                    },
703                    &language
704                )
705                .await,
706            Some(CodeLabel {
707                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
708                filter_range: 0..5,
709                runs: vec![
710                    (0..5, highlight_function),
711                    (7..10, highlight_keyword),
712                    (11..17, highlight_type),
713                    (18..19, highlight_type),
714                    (25..28, highlight_type),
715                    (29..30, highlight_type),
716                ],
717            })
718        );
719        assert_eq!(
720            adapter
721                .label_for_completion(
722                    &lsp::CompletionItem {
723                        kind: Some(lsp::CompletionItemKind::FUNCTION),
724                        label: "hello(…)".to_string(),
725                        detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
726                        ..Default::default()
727                    },
728                    &language
729                )
730                .await,
731            Some(CodeLabel {
732                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
733                filter_range: 0..5,
734                runs: vec![
735                    (0..5, highlight_function),
736                    (7..10, highlight_keyword),
737                    (11..17, highlight_type),
738                    (18..19, highlight_type),
739                    (25..28, highlight_type),
740                    (29..30, highlight_type),
741                ],
742            })
743        );
744        assert_eq!(
745            adapter
746                .label_for_completion(
747                    &lsp::CompletionItem {
748                        kind: Some(lsp::CompletionItemKind::FIELD),
749                        label: "len".to_string(),
750                        detail: Some("usize".to_string()),
751                        ..Default::default()
752                    },
753                    &language
754                )
755                .await,
756            Some(CodeLabel {
757                text: "len: usize".to_string(),
758                filter_range: 0..3,
759                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
760            })
761        );
762
763        assert_eq!(
764            adapter
765                .label_for_completion(
766                    &lsp::CompletionItem {
767                        kind: Some(lsp::CompletionItemKind::FUNCTION),
768                        label: "hello(…)".to_string(),
769                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
770                        ..Default::default()
771                    },
772                    &language
773                )
774                .await,
775            Some(CodeLabel {
776                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
777                filter_range: 0..5,
778                runs: vec![
779                    (0..5, highlight_function),
780                    (7..10, highlight_keyword),
781                    (11..17, highlight_type),
782                    (18..19, highlight_type),
783                    (25..28, highlight_type),
784                    (29..30, highlight_type),
785                ],
786            })
787        );
788    }
789
790    #[gpui::test]
791    async fn test_rust_label_for_symbol() {
792        let adapter = Arc::new(RustLspAdapter);
793        let language = language("rust", tree_sitter_rust::language());
794        let grammar = language.grammar().unwrap();
795        let theme = SyntaxTheme::new_test([
796            ("type", Hsla::default()),
797            ("keyword", Hsla::default()),
798            ("function", Hsla::default()),
799            ("property", Hsla::default()),
800        ]);
801
802        language.set_theme(&theme);
803
804        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
805        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
806        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
807
808        assert_eq!(
809            adapter
810                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
811                .await,
812            Some(CodeLabel {
813                text: "fn hello".to_string(),
814                filter_range: 3..8,
815                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
816            })
817        );
818
819        assert_eq!(
820            adapter
821                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
822                .await,
823            Some(CodeLabel {
824                text: "type World".to_string(),
825                filter_range: 5..10,
826                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
827            })
828        );
829    }
830
831    #[gpui::test]
832    async fn test_rust_autoindent(cx: &mut TestAppContext) {
833        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
834        cx.update(|cx| {
835            let test_settings = SettingsStore::test(cx);
836            cx.set_global(test_settings);
837            language::init(cx);
838            cx.update_global::<SettingsStore, _>(|store, cx| {
839                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
840                    s.defaults.tab_size = NonZeroU32::new(2);
841                });
842            });
843        });
844
845        let language = crate::language("rust", tree_sitter_rust::language());
846
847        cx.new_model(|cx| {
848            let mut buffer = Buffer::local("", cx).with_language(language, cx);
849
850            // indent between braces
851            buffer.set_text("fn a() {}", cx);
852            let ix = buffer.len() - 1;
853            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
854            assert_eq!(buffer.text(), "fn a() {\n  \n}");
855
856            // indent between braces, even after empty lines
857            buffer.set_text("fn a() {\n\n\n}", cx);
858            let ix = buffer.len() - 2;
859            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
860            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
861
862            // indent a line that continues a field expression
863            buffer.set_text("fn a() {\n  \n}", cx);
864            let ix = buffer.len() - 2;
865            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
866            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
867
868            // indent further lines that continue the field expression, even after empty lines
869            let ix = buffer.len() - 2;
870            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
871            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
872
873            // dedent the line after the field expression
874            let ix = buffer.len() - 2;
875            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
876            assert_eq!(
877                buffer.text(),
878                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
879            );
880
881            // indent inside a struct within a call
882            buffer.set_text("const a: B = c(D {});", cx);
883            let ix = buffer.len() - 3;
884            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
885            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
886
887            // indent further inside a nested call
888            let ix = buffer.len() - 4;
889            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
890            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
891
892            // keep that indent after an empty line
893            let ix = buffer.len() - 8;
894            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
895            assert_eq!(
896                buffer.text(),
897                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
898            );
899
900            buffer
901        });
902    }
903
904    #[test]
905    fn test_package_name_from_pkgid() {
906        for (input, expected) in [
907            (
908                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
909                "zed",
910            ),
911            (
912                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
913                "my-custom-package",
914            ),
915        ] {
916            assert_eq!(package_name_from_pkgid(input), Some(expected));
917        }
918    }
919
920    #[test]
921    fn test_retrieve_package_id_and_bin_name_from_metadata() {
922        for (input, absolute_path, expected) in [
923            (
924                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"}]}]}"#,
925                "/path/to/zed/src/main.rs",
926                Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
927            ),
928            (
929                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"}]}]}"#,
930                "/path/to/custom-package/src/main.rs",
931                Some((
932                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
933                    "my-custom-bin",
934                )),
935            ),
936            (
937                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"}]}]}"#,
938                "/path/to/custom-package/src/main.rs",
939                None,
940            ),
941        ] {
942            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
943
944            let absolute_path = Path::new(absolute_path);
945
946            assert_eq!(
947                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
948                expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
949            );
950        }
951    }
952}