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                ..TaskTemplate::default()
424            },
425            TaskTemplate {
426                label: "cargo check --workspace --all-targets".into(),
427                command: "cargo".into(),
428                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
429                ..TaskTemplate::default()
430            },
431            TaskTemplate {
432                label: format!(
433                    "cargo test -p {} {} -- --nocapture",
434                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
435                    VariableName::Symbol.template_value(),
436                ),
437                command: "cargo".into(),
438                args: vec![
439                    "test".into(),
440                    "-p".into(),
441                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
442                    VariableName::Symbol.template_value(),
443                    "--".into(),
444                    "--nocapture".into(),
445                ],
446                tags: vec!["rust-test".to_owned()],
447                ..TaskTemplate::default()
448            },
449            TaskTemplate {
450                label: format!(
451                    "cargo test -p {} {}",
452                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
453                    VariableName::Stem.template_value(),
454                ),
455                command: "cargo".into(),
456                args: vec![
457                    "test".into(),
458                    "-p".into(),
459                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
460                    VariableName::Stem.template_value(),
461                ],
462                tags: vec!["rust-mod-test".to_owned()],
463                ..TaskTemplate::default()
464            },
465            TaskTemplate {
466                label: format!(
467                    "cargo run -p {} --bin {}",
468                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
469                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
470                ),
471                command: "cargo".into(),
472                args: vec![
473                    "run".into(),
474                    "-p".into(),
475                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
476                    "--bin".into(),
477                    RUST_BIN_NAME_TASK_VARIABLE.template_value(),
478                ],
479                tags: vec!["rust-main".to_owned()],
480                ..TaskTemplate::default()
481            },
482            TaskTemplate {
483                label: format!(
484                    "cargo test -p {}",
485                    RUST_PACKAGE_TASK_VARIABLE.template_value()
486                ),
487                command: "cargo".into(),
488                args: vec![
489                    "test".into(),
490                    "-p".into(),
491                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
492                ],
493                ..TaskTemplate::default()
494            },
495            TaskTemplate {
496                label: "cargo run".into(),
497                command: "cargo".into(),
498                args: vec!["run".into()],
499                ..TaskTemplate::default()
500            },
501            TaskTemplate {
502                label: "cargo clean".into(),
503                command: "cargo".into(),
504                args: vec!["clean".into()],
505                ..TaskTemplate::default()
506            },
507        ]))
508    }
509}
510
511/// Part of the data structure of Cargo metadata
512#[derive(serde::Deserialize)]
513struct CargoMetadata {
514    packages: Vec<CargoPackage>,
515}
516
517#[derive(serde::Deserialize)]
518struct CargoPackage {
519    id: String,
520    targets: Vec<CargoTarget>,
521}
522
523#[derive(serde::Deserialize)]
524struct CargoTarget {
525    name: String,
526    kind: Vec<String>,
527    src_path: String,
528}
529
530fn package_name_and_bin_name_from_abs_path(abs_path: &Path) -> Option<(String, String)> {
531    let output = std::process::Command::new("cargo")
532        .current_dir(abs_path.parent()?)
533        .arg("metadata")
534        .arg("--no-deps")
535        .arg("--format-version")
536        .arg("1")
537        .output()
538        .log_err()?
539        .stdout;
540
541    let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
542
543    retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
544        |(package_id, bin_name)| {
545            let package_name = package_name_from_pkgid(&package_id);
546
547            package_name.map(|package_name| (package_name.to_owned(), bin_name))
548        },
549    )
550}
551
552fn retrieve_package_id_and_bin_name_from_metadata(
553    metadata: CargoMetadata,
554    abs_path: &Path,
555) -> Option<(String, String)> {
556    let abs_path = abs_path.to_str()?;
557
558    for package in metadata.packages {
559        for target in package.targets {
560            let is_bin = target.kind.iter().any(|kind| kind == "bin");
561            if target.src_path == abs_path && is_bin {
562                return Some((package.id, target.name));
563            }
564        }
565    }
566
567    None
568}
569
570fn human_readable_package_name(package_directory: &Path) -> Option<String> {
571    let pkgid = String::from_utf8(
572        std::process::Command::new("cargo")
573            .current_dir(package_directory)
574            .arg("pkgid")
575            .output()
576            .log_err()?
577            .stdout,
578    )
579    .ok()?;
580    Some(package_name_from_pkgid(&pkgid)?.to_owned())
581}
582
583// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
584// Output example in the root of Zed project:
585// ```bash
586// ❯ cargo pkgid zed
587// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
588// ```
589// Another variant, if a project has a custom package name or hyphen in the name:
590// ```
591// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
592// ```
593//
594// Extracts the package name from the output according to the spec:
595// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
596fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
597    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
598        match input.rsplit_once(suffix_start) {
599            Some((without_suffix, _)) => without_suffix,
600            None => input,
601        }
602    }
603
604    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
605    let package_name = match version_suffix.rsplit_once('@') {
606        Some((custom_package_name, _version)) => custom_package_name,
607        None => {
608            let host_and_path = split_off_suffix(version_prefix, '?');
609            let (_, package_name) = host_and_path.rsplit_once('/')?;
610            package_name
611        }
612    };
613    Some(package_name)
614}
615
616async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
617    maybe!(async {
618        let mut last = None;
619        let mut entries = fs::read_dir(&container_dir).await?;
620        while let Some(entry) = entries.next().await {
621            last = Some(entry?.path());
622        }
623
624        anyhow::Ok(LanguageServerBinary {
625            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
626            env: None,
627            arguments: Default::default(),
628        })
629    })
630    .await
631    .log_err()
632}
633
634#[cfg(test)]
635mod tests {
636    use std::num::NonZeroU32;
637
638    use super::*;
639    use crate::language;
640    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
641    use language::language_settings::AllLanguageSettings;
642    use settings::SettingsStore;
643    use theme::SyntaxTheme;
644
645    #[gpui::test]
646    async fn test_process_rust_diagnostics() {
647        let mut params = lsp::PublishDiagnosticsParams {
648            uri: lsp::Url::from_file_path("/a").unwrap(),
649            version: None,
650            diagnostics: vec![
651                // no newlines
652                lsp::Diagnostic {
653                    message: "use of moved value `a`".to_string(),
654                    ..Default::default()
655                },
656                // newline at the end of a code span
657                lsp::Diagnostic {
658                    message: "consider importing this struct: `use b::c;\n`".to_string(),
659                    ..Default::default()
660                },
661                // code span starting right after a newline
662                lsp::Diagnostic {
663                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
664                        .to_string(),
665                    ..Default::default()
666                },
667            ],
668        };
669        RustLspAdapter.process_diagnostics(&mut params);
670
671        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
672
673        // remove trailing newline from code span
674        assert_eq!(
675            params.diagnostics[1].message,
676            "consider importing this struct: `use b::c;`"
677        );
678
679        // do not remove newline before the start of code span
680        assert_eq!(
681            params.diagnostics[2].message,
682            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
683        );
684    }
685
686    #[gpui::test]
687    async fn test_rust_label_for_completion() {
688        let adapter = Arc::new(RustLspAdapter);
689        let language = language("rust", tree_sitter_rust::language());
690        let grammar = language.grammar().unwrap();
691        let theme = SyntaxTheme::new_test([
692            ("type", Hsla::default()),
693            ("keyword", Hsla::default()),
694            ("function", Hsla::default()),
695            ("property", Hsla::default()),
696        ]);
697
698        language.set_theme(&theme);
699
700        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
701        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
702        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
703        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
704
705        assert_eq!(
706            adapter
707                .label_for_completion(
708                    &lsp::CompletionItem {
709                        kind: Some(lsp::CompletionItemKind::FUNCTION),
710                        label: "hello(…)".to_string(),
711                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
712                        ..Default::default()
713                    },
714                    &language
715                )
716                .await,
717            Some(CodeLabel {
718                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
719                filter_range: 0..5,
720                runs: vec![
721                    (0..5, highlight_function),
722                    (7..10, highlight_keyword),
723                    (11..17, highlight_type),
724                    (18..19, highlight_type),
725                    (25..28, highlight_type),
726                    (29..30, highlight_type),
727                ],
728            })
729        );
730        assert_eq!(
731            adapter
732                .label_for_completion(
733                    &lsp::CompletionItem {
734                        kind: Some(lsp::CompletionItemKind::FUNCTION),
735                        label: "hello(…)".to_string(),
736                        detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
737                        ..Default::default()
738                    },
739                    &language
740                )
741                .await,
742            Some(CodeLabel {
743                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
744                filter_range: 0..5,
745                runs: vec![
746                    (0..5, highlight_function),
747                    (7..10, highlight_keyword),
748                    (11..17, highlight_type),
749                    (18..19, highlight_type),
750                    (25..28, highlight_type),
751                    (29..30, highlight_type),
752                ],
753            })
754        );
755        assert_eq!(
756            adapter
757                .label_for_completion(
758                    &lsp::CompletionItem {
759                        kind: Some(lsp::CompletionItemKind::FIELD),
760                        label: "len".to_string(),
761                        detail: Some("usize".to_string()),
762                        ..Default::default()
763                    },
764                    &language
765                )
766                .await,
767            Some(CodeLabel {
768                text: "len: usize".to_string(),
769                filter_range: 0..3,
770                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
771            })
772        );
773
774        assert_eq!(
775            adapter
776                .label_for_completion(
777                    &lsp::CompletionItem {
778                        kind: Some(lsp::CompletionItemKind::FUNCTION),
779                        label: "hello(…)".to_string(),
780                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
781                        ..Default::default()
782                    },
783                    &language
784                )
785                .await,
786            Some(CodeLabel {
787                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
788                filter_range: 0..5,
789                runs: vec![
790                    (0..5, highlight_function),
791                    (7..10, highlight_keyword),
792                    (11..17, highlight_type),
793                    (18..19, highlight_type),
794                    (25..28, highlight_type),
795                    (29..30, highlight_type),
796                ],
797            })
798        );
799    }
800
801    #[gpui::test]
802    async fn test_rust_label_for_symbol() {
803        let adapter = Arc::new(RustLspAdapter);
804        let language = language("rust", tree_sitter_rust::language());
805        let grammar = language.grammar().unwrap();
806        let theme = SyntaxTheme::new_test([
807            ("type", Hsla::default()),
808            ("keyword", Hsla::default()),
809            ("function", Hsla::default()),
810            ("property", Hsla::default()),
811        ]);
812
813        language.set_theme(&theme);
814
815        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
816        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
817        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
818
819        assert_eq!(
820            adapter
821                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
822                .await,
823            Some(CodeLabel {
824                text: "fn hello".to_string(),
825                filter_range: 3..8,
826                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
827            })
828        );
829
830        assert_eq!(
831            adapter
832                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
833                .await,
834            Some(CodeLabel {
835                text: "type World".to_string(),
836                filter_range: 5..10,
837                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
838            })
839        );
840    }
841
842    #[gpui::test]
843    async fn test_rust_autoindent(cx: &mut TestAppContext) {
844        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
845        cx.update(|cx| {
846            let test_settings = SettingsStore::test(cx);
847            cx.set_global(test_settings);
848            language::init(cx);
849            cx.update_global::<SettingsStore, _>(|store, cx| {
850                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
851                    s.defaults.tab_size = NonZeroU32::new(2);
852                });
853            });
854        });
855
856        let language = crate::language("rust", tree_sitter_rust::language());
857
858        cx.new_model(|cx| {
859            let mut buffer = Buffer::local("", cx).with_language(language, cx);
860
861            // indent between braces
862            buffer.set_text("fn a() {}", cx);
863            let ix = buffer.len() - 1;
864            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
865            assert_eq!(buffer.text(), "fn a() {\n  \n}");
866
867            // indent between braces, even after empty lines
868            buffer.set_text("fn a() {\n\n\n}", cx);
869            let ix = buffer.len() - 2;
870            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
871            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
872
873            // indent a line that continues a field expression
874            buffer.set_text("fn a() {\n  \n}", cx);
875            let ix = buffer.len() - 2;
876            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
877            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
878
879            // indent further lines that continue the field expression, even after empty lines
880            let ix = buffer.len() - 2;
881            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
882            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
883
884            // dedent the line after the field expression
885            let ix = buffer.len() - 2;
886            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
887            assert_eq!(
888                buffer.text(),
889                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
890            );
891
892            // indent inside a struct within a call
893            buffer.set_text("const a: B = c(D {});", cx);
894            let ix = buffer.len() - 3;
895            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
896            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
897
898            // indent further inside a nested call
899            let ix = buffer.len() - 4;
900            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
901            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
902
903            // keep that indent after an empty line
904            let ix = buffer.len() - 8;
905            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
906            assert_eq!(
907                buffer.text(),
908                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
909            );
910
911            buffer
912        });
913    }
914
915    #[test]
916    fn test_package_name_from_pkgid() {
917        for (input, expected) in [
918            (
919                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
920                "zed",
921            ),
922            (
923                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
924                "my-custom-package",
925            ),
926        ] {
927            assert_eq!(package_name_from_pkgid(input), Some(expected));
928        }
929    }
930
931    #[test]
932    fn test_retrieve_package_id_and_bin_name_from_metadata() {
933        for (input, absolute_path, expected) in [
934            (
935                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"}]}]}"#,
936                "/path/to/zed/src/main.rs",
937                Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")),
938            ),
939            (
940                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"}]}]}"#,
941                "/path/to/custom-package/src/main.rs",
942                Some((
943                    "path+file:///path/to/custom-package#my-custom-package@0.1.0",
944                    "my-custom-bin",
945                )),
946            ),
947            (
948                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"}]}]}"#,
949                "/path/to/custom-package/src/main.rs",
950                None,
951            ),
952        ] {
953            let metadata: CargoMetadata = serde_json::from_str(input).unwrap();
954
955            let absolute_path = Path::new(absolute_path);
956
957            assert_eq!(
958                retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
959                expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned()))
960            );
961        }
962    }
963}