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