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
349impl ContextProvider for RustContextProvider {
350    fn build_context(
351        &self,
352        _: &TaskVariables,
353        location: &Location,
354        cx: &mut gpui::AppContext,
355    ) -> Result<TaskVariables> {
356        let local_abs_path = location
357            .buffer
358            .read(cx)
359            .file()
360            .and_then(|file| Some(file.as_local()?.abs_path(cx)));
361        Ok(
362            if let Some(package_name) = local_abs_path
363                .as_deref()
364                .and_then(|local_abs_path| local_abs_path.parent())
365                .and_then(human_readable_package_name)
366            {
367                TaskVariables::from_iter(Some((RUST_PACKAGE_TASK_VARIABLE.clone(), package_name)))
368            } else {
369                TaskVariables::default()
370            },
371        )
372    }
373
374    fn associated_tasks(&self) -> Option<TaskTemplates> {
375        Some(TaskTemplates(vec![
376            TaskTemplate {
377                label: format!(
378                    "cargo check -p {}",
379                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
380                ),
381                command: "cargo".into(),
382                args: vec![
383                    "check".into(),
384                    "-p".into(),
385                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
386                ],
387                ..TaskTemplate::default()
388            },
389            TaskTemplate {
390                label: "cargo check --workspace --all-targets".into(),
391                command: "cargo".into(),
392                args: vec!["check".into(), "--workspace".into(), "--all-targets".into()],
393                ..TaskTemplate::default()
394            },
395            TaskTemplate {
396                label: format!(
397                    "cargo test -p {} {} -- --nocapture",
398                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
399                    VariableName::Symbol.template_value(),
400                ),
401                command: "cargo".into(),
402                args: vec![
403                    "test".into(),
404                    "-p".into(),
405                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
406                    VariableName::Symbol.template_value(),
407                    "--".into(),
408                    "--nocapture".into(),
409                ],
410                tags: vec!["rust-test".to_owned()],
411                ..TaskTemplate::default()
412            },
413            TaskTemplate {
414                label: format!(
415                    "cargo test -p {} {}",
416                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
417                    VariableName::Stem.template_value(),
418                ),
419                command: "cargo".into(),
420                args: vec![
421                    "test".into(),
422                    "-p".into(),
423                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
424                    VariableName::Stem.template_value(),
425                ],
426                tags: vec!["rust-mod-test".to_owned()],
427                ..TaskTemplate::default()
428            },
429            TaskTemplate {
430                label: format!(
431                    "cargo test -p {}",
432                    RUST_PACKAGE_TASK_VARIABLE.template_value()
433                ),
434                command: "cargo".into(),
435                args: vec![
436                    "test".into(),
437                    "-p".into(),
438                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
439                ],
440                ..TaskTemplate::default()
441            },
442            TaskTemplate {
443                label: "cargo run".into(),
444                command: "cargo".into(),
445                args: vec!["run".into()],
446                ..TaskTemplate::default()
447            },
448            TaskTemplate {
449                label: "cargo clean".into(),
450                command: "cargo".into(),
451                args: vec!["clean".into()],
452                ..TaskTemplate::default()
453            },
454        ]))
455    }
456}
457
458fn human_readable_package_name(package_directory: &Path) -> Option<String> {
459    let pkgid = String::from_utf8(
460        std::process::Command::new("cargo")
461            .current_dir(package_directory)
462            .arg("pkgid")
463            .output()
464            .log_err()?
465            .stdout,
466    )
467    .ok()?;
468    Some(package_name_from_pkgid(&pkgid)?.to_owned())
469}
470
471// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
472// Output example in the root of Zed project:
473// ```bash
474// ❯ cargo pkgid zed
475// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
476// ```
477// Another variant, if a project has a custom package name or hyphen in the name:
478// ```
479// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
480// ```
481//
482// Extracts the package name from the output according to the spec:
483// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
484fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
485    fn split_off_suffix(input: &str, suffix_start: char) -> &str {
486        match input.rsplit_once(suffix_start) {
487            Some((without_suffix, _)) => without_suffix,
488            None => input,
489        }
490    }
491
492    let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
493    let package_name = match version_suffix.rsplit_once('@') {
494        Some((custom_package_name, _version)) => custom_package_name,
495        None => {
496            let host_and_path = split_off_suffix(version_prefix, '?');
497            let (_, package_name) = host_and_path.rsplit_once('/')?;
498            package_name
499        }
500    };
501    Some(package_name)
502}
503
504async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
505    maybe!(async {
506        let mut last = None;
507        let mut entries = fs::read_dir(&container_dir).await?;
508        while let Some(entry) = entries.next().await {
509            last = Some(entry?.path());
510        }
511
512        anyhow::Ok(LanguageServerBinary {
513            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
514            env: None,
515            arguments: Default::default(),
516        })
517    })
518    .await
519    .log_err()
520}
521
522#[cfg(test)]
523mod tests {
524    use std::num::NonZeroU32;
525
526    use super::*;
527    use crate::language;
528    use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
529    use language::language_settings::AllLanguageSettings;
530    use settings::SettingsStore;
531    use theme::SyntaxTheme;
532
533    #[gpui::test]
534    async fn test_process_rust_diagnostics() {
535        let mut params = lsp::PublishDiagnosticsParams {
536            uri: lsp::Url::from_file_path("/a").unwrap(),
537            version: None,
538            diagnostics: vec![
539                // no newlines
540                lsp::Diagnostic {
541                    message: "use of moved value `a`".to_string(),
542                    ..Default::default()
543                },
544                // newline at the end of a code span
545                lsp::Diagnostic {
546                    message: "consider importing this struct: `use b::c;\n`".to_string(),
547                    ..Default::default()
548                },
549                // code span starting right after a newline
550                lsp::Diagnostic {
551                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
552                        .to_string(),
553                    ..Default::default()
554                },
555            ],
556        };
557        RustLspAdapter.process_diagnostics(&mut params);
558
559        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
560
561        // remove trailing newline from code span
562        assert_eq!(
563            params.diagnostics[1].message,
564            "consider importing this struct: `use b::c;`"
565        );
566
567        // do not remove newline before the start of code span
568        assert_eq!(
569            params.diagnostics[2].message,
570            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
571        );
572    }
573
574    #[gpui::test]
575    async fn test_rust_label_for_completion() {
576        let adapter = Arc::new(RustLspAdapter);
577        let language = language("rust", tree_sitter_rust::language());
578        let grammar = language.grammar().unwrap();
579        let theme = SyntaxTheme::new_test([
580            ("type", Hsla::default()),
581            ("keyword", Hsla::default()),
582            ("function", Hsla::default()),
583            ("property", Hsla::default()),
584        ]);
585
586        language.set_theme(&theme);
587
588        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
589        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
590        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
591        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
592
593        assert_eq!(
594            adapter
595                .label_for_completion(
596                    &lsp::CompletionItem {
597                        kind: Some(lsp::CompletionItemKind::FUNCTION),
598                        label: "hello(…)".to_string(),
599                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
600                        ..Default::default()
601                    },
602                    &language
603                )
604                .await,
605            Some(CodeLabel {
606                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
607                filter_range: 0..5,
608                runs: vec![
609                    (0..5, highlight_function),
610                    (7..10, highlight_keyword),
611                    (11..17, highlight_type),
612                    (18..19, highlight_type),
613                    (25..28, highlight_type),
614                    (29..30, highlight_type),
615                ],
616            })
617        );
618        assert_eq!(
619            adapter
620                .label_for_completion(
621                    &lsp::CompletionItem {
622                        kind: Some(lsp::CompletionItemKind::FUNCTION),
623                        label: "hello(…)".to_string(),
624                        detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
625                        ..Default::default()
626                    },
627                    &language
628                )
629                .await,
630            Some(CodeLabel {
631                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
632                filter_range: 0..5,
633                runs: vec![
634                    (0..5, highlight_function),
635                    (7..10, highlight_keyword),
636                    (11..17, highlight_type),
637                    (18..19, highlight_type),
638                    (25..28, highlight_type),
639                    (29..30, highlight_type),
640                ],
641            })
642        );
643        assert_eq!(
644            adapter
645                .label_for_completion(
646                    &lsp::CompletionItem {
647                        kind: Some(lsp::CompletionItemKind::FIELD),
648                        label: "len".to_string(),
649                        detail: Some("usize".to_string()),
650                        ..Default::default()
651                    },
652                    &language
653                )
654                .await,
655            Some(CodeLabel {
656                text: "len: usize".to_string(),
657                filter_range: 0..3,
658                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
659            })
660        );
661
662        assert_eq!(
663            adapter
664                .label_for_completion(
665                    &lsp::CompletionItem {
666                        kind: Some(lsp::CompletionItemKind::FUNCTION),
667                        label: "hello(…)".to_string(),
668                        detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
669                        ..Default::default()
670                    },
671                    &language
672                )
673                .await,
674            Some(CodeLabel {
675                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
676                filter_range: 0..5,
677                runs: vec![
678                    (0..5, highlight_function),
679                    (7..10, highlight_keyword),
680                    (11..17, highlight_type),
681                    (18..19, highlight_type),
682                    (25..28, highlight_type),
683                    (29..30, highlight_type),
684                ],
685            })
686        );
687    }
688
689    #[gpui::test]
690    async fn test_rust_label_for_symbol() {
691        let adapter = Arc::new(RustLspAdapter);
692        let language = language("rust", tree_sitter_rust::language());
693        let grammar = language.grammar().unwrap();
694        let theme = SyntaxTheme::new_test([
695            ("type", Hsla::default()),
696            ("keyword", Hsla::default()),
697            ("function", Hsla::default()),
698            ("property", Hsla::default()),
699        ]);
700
701        language.set_theme(&theme);
702
703        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
704        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
705        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
706
707        assert_eq!(
708            adapter
709                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
710                .await,
711            Some(CodeLabel {
712                text: "fn hello".to_string(),
713                filter_range: 3..8,
714                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
715            })
716        );
717
718        assert_eq!(
719            adapter
720                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
721                .await,
722            Some(CodeLabel {
723                text: "type World".to_string(),
724                filter_range: 5..10,
725                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
726            })
727        );
728    }
729
730    #[gpui::test]
731    async fn test_rust_autoindent(cx: &mut TestAppContext) {
732        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
733        cx.update(|cx| {
734            let test_settings = SettingsStore::test(cx);
735            cx.set_global(test_settings);
736            language::init(cx);
737            cx.update_global::<SettingsStore, _>(|store, cx| {
738                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
739                    s.defaults.tab_size = NonZeroU32::new(2);
740                });
741            });
742        });
743
744        let language = crate::language("rust", tree_sitter_rust::language());
745
746        cx.new_model(|cx| {
747            let mut buffer = Buffer::local("", cx).with_language(language, cx);
748
749            // indent between braces
750            buffer.set_text("fn a() {}", cx);
751            let ix = buffer.len() - 1;
752            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
753            assert_eq!(buffer.text(), "fn a() {\n  \n}");
754
755            // indent between braces, even after empty lines
756            buffer.set_text("fn a() {\n\n\n}", cx);
757            let ix = buffer.len() - 2;
758            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
759            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
760
761            // indent a line that continues a field expression
762            buffer.set_text("fn a() {\n  \n}", cx);
763            let ix = buffer.len() - 2;
764            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
765            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
766
767            // indent further lines that continue the field expression, even after empty lines
768            let ix = buffer.len() - 2;
769            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
770            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
771
772            // dedent the line after the field expression
773            let ix = buffer.len() - 2;
774            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
775            assert_eq!(
776                buffer.text(),
777                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
778            );
779
780            // indent inside a struct within a call
781            buffer.set_text("const a: B = c(D {});", cx);
782            let ix = buffer.len() - 3;
783            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
784            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
785
786            // indent further inside a nested call
787            let ix = buffer.len() - 4;
788            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
789            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
790
791            // keep that indent after an empty line
792            let ix = buffer.len() - 8;
793            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
794            assert_eq!(
795                buffer.text(),
796                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
797            );
798
799            buffer
800        });
801    }
802
803    #[test]
804    fn test_package_name_from_pkgid() {
805        for (input, expected) in [
806            (
807                "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
808                "zed",
809            ),
810            (
811                "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
812                "my-custom-package",
813            ),
814        ] {
815            assert_eq!(package_name_from_pkgid(input), Some(expected));
816        }
817    }
818}