1use anyhow::{anyhow, Context, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use client::http::{self, HttpClient, Method};
4use futures::{future::BoxFuture, FutureExt, StreamExt};
5use gpui::Task;
6pub use language::*;
7use lazy_static::lazy_static;
8use regex::Regex;
9use rust_embed::RustEmbed;
10use serde::Deserialize;
11use serde_json::json;
12use smol::fs::{self, File};
13use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
14use util::{ResultExt, TryFutureExt};
15
16#[derive(RustEmbed)]
17#[folder = "languages"]
18struct LanguageDir;
19
20struct RustLspAdapter;
21struct CLspAdapter;
22struct JsonLspAdapter;
23
24#[derive(Deserialize)]
25struct GithubRelease {
26 name: String,
27 assets: Vec<GithubReleaseAsset>,
28}
29
30#[derive(Deserialize)]
31struct GithubReleaseAsset {
32 name: String,
33 browser_download_url: http::Url,
34}
35
36impl LspAdapter for RustLspAdapter {
37 fn name(&self) -> &'static str {
38 "rust-analyzer"
39 }
40
41 fn fetch_latest_server_version(
42 &self,
43 http: Arc<dyn HttpClient>,
44 ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
45 async move {
46 let release = http
47 .send(
48 surf::RequestBuilder::new(
49 Method::Get,
50 http::Url::parse(
51 "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest",
52 )
53 .unwrap(),
54 )
55 .middleware(surf::middleware::Redirect::default())
56 .build(),
57 )
58 .await
59 .map_err(|err| anyhow!("error fetching latest release: {}", err))?
60 .body_json::<GithubRelease>()
61 .await
62 .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
63 let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
64 let asset = release
65 .assets
66 .iter()
67 .find(|asset| asset.name == asset_name)
68 .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
69 Ok(LspBinaryVersion {
70 name: release.name,
71 url: Some(asset.browser_download_url.clone()),
72 })
73 }
74 .boxed()
75 }
76
77 fn fetch_server_binary(
78 &self,
79 version: LspBinaryVersion,
80 http: Arc<dyn HttpClient>,
81 container_dir: PathBuf,
82 ) -> BoxFuture<'static, Result<PathBuf>> {
83 async move {
84 let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
85
86 if fs::metadata(&destination_path).await.is_err() {
87 let response = http
88 .send(
89 surf::RequestBuilder::new(Method::Get, version.url.unwrap())
90 .middleware(surf::middleware::Redirect::default())
91 .build(),
92 )
93 .await
94 .map_err(|err| anyhow!("error downloading release: {}", err))?;
95 let decompressed_bytes = GzipDecoder::new(response);
96 let mut file = File::create(&destination_path).await?;
97 futures::io::copy(decompressed_bytes, &mut file).await?;
98 fs::set_permissions(
99 &destination_path,
100 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
101 )
102 .await?;
103
104 if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
105 while let Some(entry) = entries.next().await {
106 if let Some(entry) = entry.log_err() {
107 let entry_path = entry.path();
108 if entry_path.as_path() != destination_path {
109 fs::remove_file(&entry_path).await.log_err();
110 }
111 }
112 }
113 }
114 }
115
116 Ok(destination_path)
117 }
118 .boxed()
119 }
120
121 fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
122 async move {
123 let mut last = None;
124 let mut entries = fs::read_dir(&container_dir).await?;
125 while let Some(entry) = entries.next().await {
126 last = Some(entry?.path());
127 }
128 last.ok_or_else(|| anyhow!("no cached binary"))
129 }
130 .log_err()
131 .boxed()
132 }
133
134 fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
135 lazy_static! {
136 static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
137 }
138
139 for diagnostic in &mut params.diagnostics {
140 for message in diagnostic
141 .related_information
142 .iter_mut()
143 .flatten()
144 .map(|info| &mut info.message)
145 .chain([&mut diagnostic.message])
146 {
147 if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
148 *message = sanitized;
149 }
150 }
151 }
152 }
153
154 fn label_for_completion(
155 &self,
156 completion: &lsp::CompletionItem,
157 language: &Language,
158 ) -> Option<CodeLabel> {
159 match completion.kind {
160 Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
161 let detail = completion.detail.as_ref().unwrap();
162 let name = &completion.label;
163 let text = format!("{}: {}", name, detail);
164 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
165 let runs = language.highlight_text(&source, 11..11 + text.len());
166 return Some(CodeLabel {
167 text,
168 runs,
169 filter_range: 0..name.len(),
170 });
171 }
172 Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
173 if completion.detail.is_some() =>
174 {
175 let detail = completion.detail.as_ref().unwrap();
176 let name = &completion.label;
177 let text = format!("{}: {}", name, detail);
178 let source = Rope::from(format!("let {} = ();", text).as_str());
179 let runs = language.highlight_text(&source, 4..4 + text.len());
180 return Some(CodeLabel {
181 text,
182 runs,
183 filter_range: 0..name.len(),
184 });
185 }
186 Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
187 if completion.detail.is_some() =>
188 {
189 lazy_static! {
190 static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
191 }
192
193 let detail = completion.detail.as_ref().unwrap();
194 if detail.starts_with("fn(") {
195 let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
196 let source = Rope::from(format!("fn {} {{}}", text).as_str());
197 let runs = language.highlight_text(&source, 3..3 + text.len());
198 return Some(CodeLabel {
199 filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
200 text,
201 runs,
202 });
203 }
204 }
205 Some(kind) => {
206 let highlight_name = match kind {
207 lsp::CompletionItemKind::STRUCT
208 | lsp::CompletionItemKind::INTERFACE
209 | lsp::CompletionItemKind::ENUM => Some("type"),
210 lsp::CompletionItemKind::ENUM_MEMBER => Some("variant"),
211 lsp::CompletionItemKind::KEYWORD => Some("keyword"),
212 lsp::CompletionItemKind::VALUE | lsp::CompletionItemKind::CONSTANT => {
213 Some("constant")
214 }
215 _ => None,
216 };
217 let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
218 let mut label = CodeLabel::plain(completion.label.clone(), None);
219 label.runs.push((
220 0..label.text.rfind('(').unwrap_or(label.text.len()),
221 highlight_id,
222 ));
223 return Some(label);
224 }
225 _ => {}
226 }
227 None
228 }
229
230 fn label_for_symbol(
231 &self,
232 name: &str,
233 kind: lsp::SymbolKind,
234 language: &Language,
235 ) -> Option<CodeLabel> {
236 let (text, filter_range, display_range) = match kind {
237 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
238 let text = format!("fn {} () {{}}", name);
239 let filter_range = 3..3 + name.len();
240 let display_range = 0..filter_range.end;
241 (text, filter_range, display_range)
242 }
243 lsp::SymbolKind::STRUCT => {
244 let text = format!("struct {} {{}}", name);
245 let filter_range = 7..7 + name.len();
246 let display_range = 0..filter_range.end;
247 (text, filter_range, display_range)
248 }
249 lsp::SymbolKind::ENUM => {
250 let text = format!("enum {} {{}}", name);
251 let filter_range = 5..5 + name.len();
252 let display_range = 0..filter_range.end;
253 (text, filter_range, display_range)
254 }
255 lsp::SymbolKind::INTERFACE => {
256 let text = format!("trait {} {{}}", name);
257 let filter_range = 6..6 + name.len();
258 let display_range = 0..filter_range.end;
259 (text, filter_range, display_range)
260 }
261 lsp::SymbolKind::CONSTANT => {
262 let text = format!("const {}: () = ();", name);
263 let filter_range = 6..6 + name.len();
264 let display_range = 0..filter_range.end;
265 (text, filter_range, display_range)
266 }
267 lsp::SymbolKind::MODULE => {
268 let text = format!("mod {} {{}}", name);
269 let filter_range = 4..4 + name.len();
270 let display_range = 0..filter_range.end;
271 (text, filter_range, display_range)
272 }
273 lsp::SymbolKind::TYPE_PARAMETER => {
274 let text = format!("type {} {{}}", name);
275 let filter_range = 5..5 + name.len();
276 let display_range = 0..filter_range.end;
277 (text, filter_range, display_range)
278 }
279 _ => return None,
280 };
281
282 Some(CodeLabel {
283 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
284 text: text[display_range].to_string(),
285 filter_range,
286 })
287 }
288}
289
290impl LspAdapter for CLspAdapter {
291 fn name(&self) -> &'static str {
292 "clangd"
293 }
294
295 fn fetch_latest_server_version(
296 &self,
297 http: Arc<dyn HttpClient>,
298 ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
299 async move {
300 let release = http
301 .send(
302 surf::RequestBuilder::new(
303 Method::Get,
304 http::Url::parse(
305 "https://api.github.com/repos/clangd/clangd/releases/latest",
306 )
307 .unwrap(),
308 )
309 .middleware(surf::middleware::Redirect::default())
310 .build(),
311 )
312 .await
313 .map_err(|err| anyhow!("error fetching latest release: {}", err))?
314 .body_json::<GithubRelease>()
315 .await
316 .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
317 let asset_name = format!("clangd-mac-{}.zip", release.name);
318 let asset = release
319 .assets
320 .iter()
321 .find(|asset| asset.name == asset_name)
322 .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
323 Ok(LspBinaryVersion {
324 name: release.name,
325 url: Some(asset.browser_download_url.clone()),
326 })
327 }
328 .boxed()
329 }
330
331 fn fetch_server_binary(
332 &self,
333 version: LspBinaryVersion,
334 http: Arc<dyn HttpClient>,
335 container_dir: PathBuf,
336 ) -> BoxFuture<'static, Result<PathBuf>> {
337 async move {
338 let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
339 let version_dir = container_dir.join(format!("clangd_{}", version.name));
340 let binary_path = version_dir.join("bin/clangd");
341
342 if fs::metadata(&binary_path).await.is_err() {
343 let response = http
344 .send(
345 surf::RequestBuilder::new(Method::Get, version.url.unwrap())
346 .middleware(surf::middleware::Redirect::default())
347 .build(),
348 )
349 .await
350 .map_err(|err| anyhow!("error downloading release: {}", err))?;
351 let mut file = File::create(&zip_path).await?;
352 if !response.status().is_success() {
353 Err(anyhow!(
354 "download failed with status {}",
355 response.status().to_string()
356 ))?;
357 }
358 futures::io::copy(response, &mut file).await?;
359
360 let unzip_status = smol::process::Command::new("unzip")
361 .current_dir(&container_dir)
362 .arg(&zip_path)
363 .output()
364 .await?
365 .status;
366 if !unzip_status.success() {
367 Err(anyhow!("failed to unzip clangd archive"))?;
368 }
369
370 if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
371 while let Some(entry) = entries.next().await {
372 if let Some(entry) = entry.log_err() {
373 let entry_path = entry.path();
374 if entry_path.as_path() != version_dir {
375 fs::remove_dir_all(&entry_path).await.log_err();
376 }
377 }
378 }
379 }
380 }
381
382 Ok(binary_path)
383 }
384 .boxed()
385 }
386
387 fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
388 async move {
389 let mut last_clangd_dir = None;
390 let mut entries = fs::read_dir(&container_dir).await?;
391 while let Some(entry) = entries.next().await {
392 let entry = entry?;
393 if entry.file_type().await?.is_dir() {
394 last_clangd_dir = Some(entry.path());
395 }
396 }
397 let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
398 let clangd_bin = clangd_dir.join("bin/clangd");
399 if clangd_bin.exists() {
400 Ok(clangd_bin)
401 } else {
402 Err(anyhow!(
403 "missing clangd binary in directory {:?}",
404 clangd_dir
405 ))
406 }
407 }
408 .log_err()
409 .boxed()
410 }
411
412 fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
413}
414
415impl JsonLspAdapter {
416 const BIN_PATH: &'static str =
417 "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
418}
419
420impl LspAdapter for JsonLspAdapter {
421 fn name(&self) -> &'static str {
422 "vscode-json-languageserver"
423 }
424
425 fn server_args(&self) -> &[&str] {
426 &["--stdio"]
427 }
428
429 fn fetch_latest_server_version(
430 &self,
431 _: Arc<dyn HttpClient>,
432 ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
433 async move {
434 #[derive(Deserialize)]
435 struct NpmInfo {
436 versions: Vec<String>,
437 }
438
439 let output = smol::process::Command::new("npm")
440 .args(["info", "vscode-json-languageserver", "--json"])
441 .output()
442 .await?;
443 if !output.status.success() {
444 Err(anyhow!("failed to execute npm info"))?;
445 }
446 let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
447
448 Ok(LspBinaryVersion {
449 name: info
450 .versions
451 .pop()
452 .ok_or_else(|| anyhow!("no versions found in npm info"))?,
453 url: Default::default(),
454 })
455 }
456 .boxed()
457 }
458
459 fn fetch_server_binary(
460 &self,
461 version: LspBinaryVersion,
462 _: Arc<dyn HttpClient>,
463 container_dir: PathBuf,
464 ) -> BoxFuture<'static, Result<PathBuf>> {
465 async move {
466 let version_dir = container_dir.join(&version.name);
467 fs::create_dir_all(&version_dir)
468 .await
469 .context("failed to create version directory")?;
470 let binary_path = version_dir.join(Self::BIN_PATH);
471
472 if fs::metadata(&binary_path).await.is_err() {
473 let output = smol::process::Command::new("npm")
474 .current_dir(&version_dir)
475 .arg("install")
476 .arg(format!("vscode-json-languageserver@{}", version.name))
477 .output()
478 .await
479 .context("failed to run npm install")?;
480 if !output.status.success() {
481 Err(anyhow!("failed to install vscode-json-languageserver"))?;
482 }
483
484 if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
485 while let Some(entry) = entries.next().await {
486 if let Some(entry) = entry.log_err() {
487 let entry_path = entry.path();
488 if entry_path.as_path() != version_dir {
489 fs::remove_dir_all(&entry_path).await.log_err();
490 }
491 }
492 }
493 }
494 }
495
496 Ok(binary_path)
497 }
498 .boxed()
499 }
500
501 fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
502 async move {
503 let mut last_version_dir = None;
504 let mut entries = fs::read_dir(&container_dir).await?;
505 while let Some(entry) = entries.next().await {
506 let entry = entry?;
507 if entry.file_type().await?.is_dir() {
508 last_version_dir = Some(entry.path());
509 }
510 }
511 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
512 let bin_path = last_version_dir.join(Self::BIN_PATH);
513 if bin_path.exists() {
514 Ok(bin_path)
515 } else {
516 Err(anyhow!(
517 "missing executable in directory {:?}",
518 last_version_dir
519 ))
520 }
521 }
522 .log_err()
523 .boxed()
524 }
525
526 fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
527
528 fn initialization_options(&self) -> Option<serde_json::Value> {
529 Some(json!({
530 "provideFormatter": true
531 }))
532 }
533}
534
535pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
536 let mut languages = LanguageRegistry::new(login_shell_env_loaded);
537 languages.set_language_server_download_dir(
538 dirs::home_dir()
539 .expect("failed to determine home directory")
540 .join(".zed"),
541 );
542 languages.add(Arc::new(c()));
543 languages.add(Arc::new(json()));
544 languages.add(Arc::new(rust()));
545 languages.add(Arc::new(markdown()));
546 languages
547}
548
549fn rust() -> Language {
550 let grammar = tree_sitter_rust::language();
551 let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
552 Language::new(config, Some(grammar))
553 .with_highlights_query(load_query("rust/highlights.scm").as_ref())
554 .unwrap()
555 .with_brackets_query(load_query("rust/brackets.scm").as_ref())
556 .unwrap()
557 .with_indents_query(load_query("rust/indents.scm").as_ref())
558 .unwrap()
559 .with_outline_query(load_query("rust/outline.scm").as_ref())
560 .unwrap()
561 .with_lsp_adapter(RustLspAdapter)
562}
563
564fn c() -> Language {
565 let grammar = tree_sitter_c::language();
566 let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap();
567 Language::new(config, Some(grammar))
568 .with_highlights_query(load_query("c/highlights.scm").as_ref())
569 .unwrap()
570 .with_brackets_query(load_query("c/brackets.scm").as_ref())
571 .unwrap()
572 .with_indents_query(load_query("c/indents.scm").as_ref())
573 .unwrap()
574 .with_outline_query(load_query("c/outline.scm").as_ref())
575 .unwrap()
576 .with_lsp_adapter(CLspAdapter)
577}
578
579fn json() -> Language {
580 let grammar = tree_sitter_json::language();
581 let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap();
582 Language::new(config, Some(grammar))
583 .with_highlights_query(load_query("json/highlights.scm").as_ref())
584 .unwrap()
585 .with_brackets_query(load_query("json/brackets.scm").as_ref())
586 .unwrap()
587 .with_indents_query(load_query("json/indents.scm").as_ref())
588 .unwrap()
589 .with_outline_query(load_query("json/outline.scm").as_ref())
590 .unwrap()
591 .with_lsp_adapter(JsonLspAdapter)
592}
593
594fn markdown() -> Language {
595 let grammar = tree_sitter_markdown::language();
596 let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap();
597 Language::new(config, Some(grammar))
598 .with_highlights_query(load_query("markdown/highlights.scm").as_ref())
599 .unwrap()
600}
601
602fn load_query(path: &str) -> Cow<'static, str> {
603 match LanguageDir::get(path).unwrap().data {
604 Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
605 Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612 use gpui::color::Color;
613 use language::LspAdapter;
614 use theme::SyntaxTheme;
615
616 #[test]
617 fn test_process_rust_diagnostics() {
618 let mut params = lsp::PublishDiagnosticsParams {
619 uri: lsp::Url::from_file_path("/a").unwrap(),
620 version: None,
621 diagnostics: vec![
622 // no newlines
623 lsp::Diagnostic {
624 message: "use of moved value `a`".to_string(),
625 ..Default::default()
626 },
627 // newline at the end of a code span
628 lsp::Diagnostic {
629 message: "consider importing this struct: `use b::c;\n`".to_string(),
630 ..Default::default()
631 },
632 // code span starting right after a newline
633 lsp::Diagnostic {
634 message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
635 .to_string(),
636 ..Default::default()
637 },
638 ],
639 };
640 RustLspAdapter.process_diagnostics(&mut params);
641
642 assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
643
644 // remove trailing newline from code span
645 assert_eq!(
646 params.diagnostics[1].message,
647 "consider importing this struct: `use b::c;`"
648 );
649
650 // do not remove newline before the start of code span
651 assert_eq!(
652 params.diagnostics[2].message,
653 "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
654 );
655 }
656
657 #[test]
658 fn test_rust_label_for_completion() {
659 let language = rust();
660 let grammar = language.grammar().unwrap();
661 let theme = SyntaxTheme::new(vec![
662 ("type".into(), Color::green().into()),
663 ("keyword".into(), Color::blue().into()),
664 ("function".into(), Color::red().into()),
665 ("property".into(), Color::white().into()),
666 ]);
667
668 language.set_theme(&theme);
669
670 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
671 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
672 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
673 let highlight_field = grammar.highlight_id_for_name("property").unwrap();
674
675 assert_eq!(
676 language.label_for_completion(&lsp::CompletionItem {
677 kind: Some(lsp::CompletionItemKind::FUNCTION),
678 label: "hello(…)".to_string(),
679 detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
680 ..Default::default()
681 }),
682 Some(CodeLabel {
683 text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
684 filter_range: 0..5,
685 runs: vec![
686 (0..5, highlight_function),
687 (7..10, highlight_keyword),
688 (11..17, highlight_type),
689 (18..19, highlight_type),
690 (25..28, highlight_type),
691 (29..30, highlight_type),
692 ],
693 })
694 );
695
696 assert_eq!(
697 language.label_for_completion(&lsp::CompletionItem {
698 kind: Some(lsp::CompletionItemKind::FIELD),
699 label: "len".to_string(),
700 detail: Some("usize".to_string()),
701 ..Default::default()
702 }),
703 Some(CodeLabel {
704 text: "len: usize".to_string(),
705 filter_range: 0..3,
706 runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
707 })
708 );
709
710 assert_eq!(
711 language.label_for_completion(&lsp::CompletionItem {
712 kind: Some(lsp::CompletionItemKind::FUNCTION),
713 label: "hello(…)".to_string(),
714 detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
715 ..Default::default()
716 }),
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 }
731
732 #[test]
733 fn test_rust_label_for_symbol() {
734 let language = rust();
735 let grammar = language.grammar().unwrap();
736 let theme = SyntaxTheme::new(vec![
737 ("type".into(), Color::green().into()),
738 ("keyword".into(), Color::blue().into()),
739 ("function".into(), Color::red().into()),
740 ("property".into(), Color::white().into()),
741 ]);
742
743 language.set_theme(&theme);
744
745 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
746 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
747 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
748
749 assert_eq!(
750 language.label_for_symbol("hello", lsp::SymbolKind::FUNCTION),
751 Some(CodeLabel {
752 text: "fn hello".to_string(),
753 filter_range: 3..8,
754 runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
755 })
756 );
757
758 assert_eq!(
759 language.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER),
760 Some(CodeLabel {
761 text: "type World".to_string(),
762 filter_range: 5..10,
763 runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
764 })
765 );
766 }
767}