rust.rs

  1use anyhow::{anyhow, bail, Result};
  2use async_compression::futures::bufread::GzipDecoder;
  3use async_trait::async_trait;
  4use futures::{io::BufReader, StreamExt};
  5pub use language::*;
  6use lazy_static::lazy_static;
  7use lsp::LanguageServerBinary;
  8use regex::Regex;
  9use smol::fs::{self, File};
 10use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
 11use util::{
 12    async_maybe,
 13    fs::remove_matching,
 14    github::{latest_github_release, GitHubLspBinaryVersion},
 15    ResultExt,
 16};
 17
 18pub struct RustLspAdapter;
 19
 20#[async_trait]
 21impl LspAdapter for RustLspAdapter {
 22    fn name(&self) -> LanguageServerName {
 23        LanguageServerName("rust-analyzer".into())
 24    }
 25
 26    fn short_name(&self) -> &'static str {
 27        "rust"
 28    }
 29
 30    async fn fetch_latest_server_version(
 31        &self,
 32        delegate: &dyn LspAdapterDelegate,
 33    ) -> Result<Box<dyn 'static + Send + Any>> {
 34        let release = latest_github_release(
 35            "rust-lang/rust-analyzer",
 36            true,
 37            false,
 38            delegate.http_client(),
 39        )
 40        .await?;
 41        let os = match consts::OS {
 42            "macos" => "apple-darwin",
 43            "linux" => "unknown-linux-gnu",
 44            "windows" => "pc-windows-msvc",
 45            other => bail!("Running on unsupported os: {other}"),
 46        };
 47        let asset_name = format!("rust-analyzer-{}-{os}.gz", consts::ARCH);
 48        let asset = release
 49            .assets
 50            .iter()
 51            .find(|asset| asset.name == asset_name)
 52            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 53        Ok(Box::new(GitHubLspBinaryVersion {
 54            name: release.tag_name,
 55            url: asset.browser_download_url.clone(),
 56        }))
 57    }
 58
 59    async fn fetch_server_binary(
 60        &self,
 61        version: Box<dyn 'static + Send + Any>,
 62        container_dir: PathBuf,
 63        delegate: &dyn LspAdapterDelegate,
 64    ) -> Result<LanguageServerBinary> {
 65        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 66        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 67
 68        if fs::metadata(&destination_path).await.is_err() {
 69            let mut response = delegate
 70                .http_client()
 71                .get(&version.url, Default::default(), true)
 72                .await
 73                .map_err(|err| anyhow!("error downloading release: {}", err))?;
 74            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
 75            let mut file = File::create(&destination_path).await?;
 76            futures::io::copy(decompressed_bytes, &mut file).await?;
 77            // todo!("windows")
 78            #[cfg(not(windows))]
 79            {
 80                fs::set_permissions(
 81                    &destination_path,
 82                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
 83                )
 84                .await?;
 85            }
 86
 87            remove_matching(&container_dir, |entry| entry != destination_path).await;
 88        }
 89
 90        Ok(LanguageServerBinary {
 91            path: destination_path,
 92            arguments: Default::default(),
 93        })
 94    }
 95
 96    async fn cached_server_binary(
 97        &self,
 98        container_dir: PathBuf,
 99        _: &dyn LspAdapterDelegate,
100    ) -> Option<LanguageServerBinary> {
101        get_cached_server_binary(container_dir).await
102    }
103
104    async fn installation_test_binary(
105        &self,
106        container_dir: PathBuf,
107    ) -> Option<LanguageServerBinary> {
108        get_cached_server_binary(container_dir)
109            .await
110            .map(|mut binary| {
111                binary.arguments = vec!["--help".into()];
112                binary
113            })
114    }
115
116    fn disk_based_diagnostic_sources(&self) -> Vec<String> {
117        vec!["rustc".into()]
118    }
119
120    fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
121        Some("rust-analyzer/flycheck".into())
122    }
123
124    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
125        lazy_static! {
126            static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
127        }
128
129        for diagnostic in &mut params.diagnostics {
130            for message in diagnostic
131                .related_information
132                .iter_mut()
133                .flatten()
134                .map(|info| &mut info.message)
135                .chain([&mut diagnostic.message])
136            {
137                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
138                    *message = sanitized;
139                }
140            }
141        }
142    }
143
144    async fn label_for_completion(
145        &self,
146        completion: &lsp::CompletionItem,
147        language: &Arc<Language>,
148    ) -> Option<CodeLabel> {
149        match completion.kind {
150            Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
151                let detail = completion.detail.as_ref().unwrap();
152                let name = &completion.label;
153                let text = format!("{}: {}", name, detail);
154                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
155                let runs = language.highlight_text(&source, 11..11 + text.len());
156                return Some(CodeLabel {
157                    text,
158                    runs,
159                    filter_range: 0..name.len(),
160                });
161            }
162            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
163                if completion.detail.is_some()
164                    && completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
165            {
166                let detail = completion.detail.as_ref().unwrap();
167                let name = &completion.label;
168                let text = format!("{}: {}", name, detail);
169                let source = Rope::from(format!("let {} = ();", text).as_str());
170                let runs = language.highlight_text(&source, 4..4 + text.len());
171                return Some(CodeLabel {
172                    text,
173                    runs,
174                    filter_range: 0..name.len(),
175                });
176            }
177            Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
178                if completion.detail.is_some() =>
179            {
180                lazy_static! {
181                    static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
182                }
183                let detail = completion.detail.as_ref().unwrap();
184                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
185                let prefix = FUNCTION_PREFIXES
186                    .iter()
187                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
188                // fn keyword should be followed by opening parenthesis.
189                if let Some((prefix, suffix)) = prefix {
190                    if suffix.starts_with('(') {
191                        let text = REGEX.replace(&completion.label, suffix).to_string();
192                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
193                        let run_start = prefix.len() + 1;
194                        let runs =
195                            language.highlight_text(&source, run_start..run_start + text.len());
196                        return Some(CodeLabel {
197                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
198                            text,
199                            runs,
200                        });
201                    }
202                }
203            }
204            Some(kind) => {
205                let highlight_name = match kind {
206                    lsp::CompletionItemKind::STRUCT
207                    | lsp::CompletionItemKind::INTERFACE
208                    | lsp::CompletionItemKind::ENUM => Some("type"),
209                    lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
210                    lsp::CompletionItemKind::KEYWORD => Some("keyword"),
211                    lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
212                        Some("constant")
213                    }
214                    _ => None,
215                };
216                let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
217                let mut label = CodeLabel::plain(completion.label.clone(), None);
218                label.runs.push((
219                    0..label.text.rfind('(').unwrap_or(label.text.len()),
220                    highlight_id,
221                ));
222                return Some(label);
223            }
224            _ => {}
225        }
226        None
227    }
228
229    async fn label_for_symbol(
230        &self,
231        name: &str,
232        kind: lsp::SymbolKind,
233        language: &Arc<Language>,
234    ) -> Option<CodeLabel> {
235        let (text, filter_range, display_range) = match kind {
236            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
237                let text = format!("fn {} () {{}}", name);
238                let filter_range = 3..3 + name.len();
239                let display_range = 0..filter_range.end;
240                (text, filter_range, display_range)
241            }
242            lsp::SymbolKind::STRUCT => {
243                let text = format!("struct {} {{}}", name);
244                let filter_range = 7..7 + name.len();
245                let display_range = 0..filter_range.end;
246                (text, filter_range, display_range)
247            }
248            lsp::SymbolKind::ENUM => {
249                let text = format!("enum {} {{}}", name);
250                let filter_range = 5..5 + name.len();
251                let display_range = 0..filter_range.end;
252                (text, filter_range, display_range)
253            }
254            lsp::SymbolKind::INTERFACE => {
255                let text = format!("trait {} {{}}", name);
256                let filter_range = 6..6 + name.len();
257                let display_range = 0..filter_range.end;
258                (text, filter_range, display_range)
259            }
260            lsp::SymbolKind::CONSTANT => {
261                let text = format!("const {}: () = ();", name);
262                let filter_range = 6..6 + name.len();
263                let display_range = 0..filter_range.end;
264                (text, filter_range, display_range)
265            }
266            lsp::SymbolKind::MODULE => {
267                let text = format!("mod {} {{}}", name);
268                let filter_range = 4..4 + name.len();
269                let display_range = 0..filter_range.end;
270                (text, filter_range, display_range)
271            }
272            lsp::SymbolKind::TYPE_PARAMETER => {
273                let text = format!("type {} {{}}", name);
274                let filter_range = 5..5 + name.len();
275                let display_range = 0..filter_range.end;
276                (text, filter_range, display_range)
277            }
278            _ => return None,
279        };
280
281        Some(CodeLabel {
282            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
283            text: text[display_range].to_string(),
284            filter_range,
285        })
286    }
287}
288
289async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
290    async_maybe!({
291        let mut last = None;
292        let mut entries = fs::read_dir(&container_dir).await?;
293        while let Some(entry) = entries.next().await {
294            last = Some(entry?.path());
295        }
296
297        anyhow::Ok(LanguageServerBinary {
298            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
299            arguments: Default::default(),
300        })
301    })
302    .await
303    .log_err()
304}
305
306#[cfg(test)]
307mod tests {
308    use std::num::NonZeroU32;
309
310    use super::*;
311    use crate::languages::language;
312    use gpui::{Context, Hsla, TestAppContext};
313    use language::language_settings::AllLanguageSettings;
314    use settings::SettingsStore;
315    use text::BufferId;
316    use theme::SyntaxTheme;
317
318    #[gpui::test]
319    async fn test_process_rust_diagnostics() {
320        let mut params = lsp::PublishDiagnosticsParams {
321            uri: lsp::Url::from_file_path("/a").unwrap(),
322            version: None,
323            diagnostics: vec![
324                // no newlines
325                lsp::Diagnostic {
326                    message: "use of moved value `a`".to_string(),
327                    ..Default::default()
328                },
329                // newline at the end of a code span
330                lsp::Diagnostic {
331                    message: "consider importing this struct: `use b::c;\n`".to_string(),
332                    ..Default::default()
333                },
334                // code span starting right after a newline
335                lsp::Diagnostic {
336                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
337                        .to_string(),
338                    ..Default::default()
339                },
340            ],
341        };
342        RustLspAdapter.process_diagnostics(&mut params);
343
344        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
345
346        // remove trailing newline from code span
347        assert_eq!(
348            params.diagnostics[1].message,
349            "consider importing this struct: `use b::c;`"
350        );
351
352        // do not remove newline before the start of code span
353        assert_eq!(
354            params.diagnostics[2].message,
355            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
356        );
357    }
358
359    #[gpui::test]
360    async fn test_rust_label_for_completion() {
361        let language = language(
362            "rust",
363            tree_sitter_rust::language(),
364            Some(Arc::new(RustLspAdapter)),
365        )
366        .await;
367        let grammar = language.grammar().unwrap();
368        let theme = SyntaxTheme::new_test([
369            ("type", Hsla::default()),
370            ("keyword", Hsla::default()),
371            ("function", Hsla::default()),
372            ("property", Hsla::default()),
373        ]);
374
375        language.set_theme(&theme);
376
377        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
378        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
379        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
380        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
381
382        assert_eq!(
383            language
384                .label_for_completion(&lsp::CompletionItem {
385                    kind: Some(lsp::CompletionItemKind::FUNCTION),
386                    label: "hello(…)".to_string(),
387                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
388                    ..Default::default()
389                })
390                .await,
391            Some(CodeLabel {
392                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
393                filter_range: 0..5,
394                runs: vec![
395                    (0..5, highlight_function),
396                    (7..10, highlight_keyword),
397                    (11..17, highlight_type),
398                    (18..19, highlight_type),
399                    (25..28, highlight_type),
400                    (29..30, highlight_type),
401                ],
402            })
403        );
404        assert_eq!(
405            language
406                .label_for_completion(&lsp::CompletionItem {
407                    kind: Some(lsp::CompletionItemKind::FUNCTION),
408                    label: "hello(…)".to_string(),
409                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
410                    ..Default::default()
411                })
412                .await,
413            Some(CodeLabel {
414                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
415                filter_range: 0..5,
416                runs: vec![
417                    (0..5, highlight_function),
418                    (7..10, highlight_keyword),
419                    (11..17, highlight_type),
420                    (18..19, highlight_type),
421                    (25..28, highlight_type),
422                    (29..30, highlight_type),
423                ],
424            })
425        );
426        assert_eq!(
427            language
428                .label_for_completion(&lsp::CompletionItem {
429                    kind: Some(lsp::CompletionItemKind::FIELD),
430                    label: "len".to_string(),
431                    detail: Some("usize".to_string()),
432                    ..Default::default()
433                })
434                .await,
435            Some(CodeLabel {
436                text: "len: usize".to_string(),
437                filter_range: 0..3,
438                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
439            })
440        );
441
442        assert_eq!(
443            language
444                .label_for_completion(&lsp::CompletionItem {
445                    kind: Some(lsp::CompletionItemKind::FUNCTION),
446                    label: "hello(…)".to_string(),
447                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
448                    ..Default::default()
449                })
450                .await,
451            Some(CodeLabel {
452                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
453                filter_range: 0..5,
454                runs: vec![
455                    (0..5, highlight_function),
456                    (7..10, highlight_keyword),
457                    (11..17, highlight_type),
458                    (18..19, highlight_type),
459                    (25..28, highlight_type),
460                    (29..30, highlight_type),
461                ],
462            })
463        );
464    }
465
466    #[gpui::test]
467    async fn test_rust_label_for_symbol() {
468        let language = language(
469            "rust",
470            tree_sitter_rust::language(),
471            Some(Arc::new(RustLspAdapter)),
472        )
473        .await;
474        let grammar = language.grammar().unwrap();
475        let theme = SyntaxTheme::new_test([
476            ("type", Hsla::default()),
477            ("keyword", Hsla::default()),
478            ("function", Hsla::default()),
479            ("property", Hsla::default()),
480        ]);
481
482        language.set_theme(&theme);
483
484        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
485        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
486        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
487
488        assert_eq!(
489            language
490                .label_for_symbol("hello", lsp::SymbolKind::FUNCTION)
491                .await,
492            Some(CodeLabel {
493                text: "fn hello".to_string(),
494                filter_range: 3..8,
495                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
496            })
497        );
498
499        assert_eq!(
500            language
501                .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER)
502                .await,
503            Some(CodeLabel {
504                text: "type World".to_string(),
505                filter_range: 5..10,
506                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
507            })
508        );
509    }
510
511    #[gpui::test]
512    async fn test_rust_autoindent(cx: &mut TestAppContext) {
513        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
514        cx.update(|cx| {
515            let test_settings = SettingsStore::test(cx);
516            cx.set_global(test_settings);
517            language::init(cx);
518            cx.update_global::<SettingsStore, _>(|store, cx| {
519                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
520                    s.defaults.tab_size = NonZeroU32::new(2);
521                });
522            });
523        });
524
525        let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;
526
527        cx.new_model(|cx| {
528            let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
529                .with_language(language, cx);
530
531            // indent between braces
532            buffer.set_text("fn a() {}", cx);
533            let ix = buffer.len() - 1;
534            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
535            assert_eq!(buffer.text(), "fn a() {\n  \n}");
536
537            // indent between braces, even after empty lines
538            buffer.set_text("fn a() {\n\n\n}", cx);
539            let ix = buffer.len() - 2;
540            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
541            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
542
543            // indent a line that continues a field expression
544            buffer.set_text("fn a() {\n  \n}", cx);
545            let ix = buffer.len() - 2;
546            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
547            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
548
549            // indent further lines that continue the field expression, even after empty lines
550            let ix = buffer.len() - 2;
551            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
552            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
553
554            // dedent the line after the field expression
555            let ix = buffer.len() - 2;
556            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
557            assert_eq!(
558                buffer.text(),
559                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
560            );
561
562            // indent inside a struct within a call
563            buffer.set_text("const a: B = c(D {});", cx);
564            let ix = buffer.len() - 3;
565            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
566            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
567
568            // indent further inside a nested call
569            let ix = buffer.len() - 4;
570            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
571            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
572
573            // keep that indent after an empty line
574            let ix = buffer.len() - 8;
575            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
576            assert_eq!(
577                buffer.text(),
578                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
579            );
580
581            buffer
582        });
583    }
584}