1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::{App, AsyncApp, Task};
6use http_client::github::latest_github_release;
7pub use language::*;
8use lsp::{LanguageServerBinary, LanguageServerName};
9use project::Fs;
10use regex::Regex;
11use serde_json::json;
12use smol::fs;
13use std::{
14 any::Any,
15 borrow::Cow,
16 ffi::{OsStr, OsString},
17 ops::Range,
18 path::PathBuf,
19 process::Output,
20 str,
21 sync::{
22 Arc, LazyLock,
23 atomic::{AtomicBool, Ordering::SeqCst},
24 },
25};
26use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
27use util::{ResultExt, fs::remove_matching, maybe};
28
29fn server_binary_arguments() -> Vec<OsString> {
30 vec!["-mode=stdio".into()]
31}
32
33#[derive(Copy, Clone)]
34pub struct GoLspAdapter;
35
36impl GoLspAdapter {
37 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls");
38}
39
40static VERSION_REGEX: LazyLock<Regex> =
41 LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX"));
42
43static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
44 Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
45});
46
47const BINARY: &str = if cfg!(target_os = "windows") {
48 "gopls.exe"
49} else {
50 "gopls"
51};
52
53#[async_trait(?Send)]
54impl super::LspAdapter for GoLspAdapter {
55 fn name(&self) -> LanguageServerName {
56 Self::SERVER_NAME.clone()
57 }
58
59 async fn fetch_latest_server_version(
60 &self,
61 delegate: &dyn LspAdapterDelegate,
62 ) -> Result<Box<dyn 'static + Send + Any>> {
63 let release =
64 latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
65 let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
66 if version.is_none() {
67 log::warn!(
68 "couldn't infer gopls version from GitHub release tag name '{}'",
69 release.tag_name
70 );
71 }
72 Ok(Box::new(version) as Box<_>)
73 }
74
75 async fn check_if_user_installed(
76 &self,
77 delegate: &dyn LspAdapterDelegate,
78 _: Arc<dyn LanguageToolchainStore>,
79 _: &AsyncApp,
80 ) -> Option<LanguageServerBinary> {
81 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
82 Some(LanguageServerBinary {
83 path,
84 arguments: server_binary_arguments(),
85 env: None,
86 })
87 }
88
89 fn will_fetch_server(
90 &self,
91 delegate: &Arc<dyn LspAdapterDelegate>,
92 cx: &mut AsyncApp,
93 ) -> Option<Task<Result<()>>> {
94 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
95
96 const NOTIFICATION_MESSAGE: &str =
97 "Could not install the Go language server `gopls`, because `go` was not found.";
98
99 let delegate = delegate.clone();
100 Some(cx.spawn(async move |cx| {
101 if delegate.which("go".as_ref()).await.is_none() {
102 if DID_SHOW_NOTIFICATION
103 .compare_exchange(false, true, SeqCst, SeqCst)
104 .is_ok()
105 {
106 cx.update(|cx| {
107 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
108 })?
109 }
110 anyhow::bail!("cannot install gopls");
111 }
112 Ok(())
113 }))
114 }
115
116 async fn fetch_server_binary(
117 &self,
118 version: Box<dyn 'static + Send + Any>,
119 container_dir: PathBuf,
120 delegate: &dyn LspAdapterDelegate,
121 ) -> Result<LanguageServerBinary> {
122 let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
123 let go_version_output = util::command::new_smol_command(&go)
124 .args(["version"])
125 .output()
126 .await
127 .context("failed to get go version via `go version` command`")?;
128 let go_version = parse_version_output(&go_version_output)?;
129 let version = version.downcast::<Option<String>>().unwrap();
130 let this = *self;
131
132 if let Some(version) = *version {
133 let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
134 if let Ok(metadata) = fs::metadata(&binary_path).await {
135 if metadata.is_file() {
136 remove_matching(&container_dir, |entry| {
137 entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
138 })
139 .await;
140
141 return Ok(LanguageServerBinary {
142 path: binary_path.to_path_buf(),
143 arguments: server_binary_arguments(),
144 env: None,
145 });
146 }
147 }
148 } else if let Some(path) = this
149 .cached_server_binary(container_dir.clone(), delegate)
150 .await
151 {
152 return Ok(path);
153 }
154
155 let gobin_dir = container_dir.join("gobin");
156 fs::create_dir_all(&gobin_dir).await?;
157 let install_output = util::command::new_smol_command(go)
158 .env("GO111MODULE", "on")
159 .env("GOBIN", &gobin_dir)
160 .args(["install", "golang.org/x/tools/gopls@latest"])
161 .output()
162 .await?;
163
164 if !install_output.status.success() {
165 log::error!(
166 "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}",
167 String::from_utf8_lossy(&install_output.stdout),
168 String::from_utf8_lossy(&install_output.stderr)
169 );
170 anyhow::bail!(
171 "failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information."
172 );
173 }
174
175 let installed_binary_path = gobin_dir.join(BINARY);
176 let version_output = util::command::new_smol_command(&installed_binary_path)
177 .arg("version")
178 .output()
179 .await
180 .context("failed to run installed gopls binary")?;
181 let gopls_version = parse_version_output(&version_output)?;
182 let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}"));
183 fs::rename(&installed_binary_path, &binary_path).await?;
184
185 Ok(LanguageServerBinary {
186 path: binary_path.to_path_buf(),
187 arguments: server_binary_arguments(),
188 env: None,
189 })
190 }
191
192 async fn cached_server_binary(
193 &self,
194 container_dir: PathBuf,
195 _: &dyn LspAdapterDelegate,
196 ) -> Option<LanguageServerBinary> {
197 get_cached_server_binary(container_dir).await
198 }
199
200 async fn initialization_options(
201 self: Arc<Self>,
202 _: &dyn Fs,
203 _: &Arc<dyn LspAdapterDelegate>,
204 ) -> Result<Option<serde_json::Value>> {
205 Ok(Some(json!({
206 "usePlaceholders": true,
207 "hints": {
208 "assignVariableTypes": true,
209 "compositeLiteralFields": true,
210 "compositeLiteralTypes": true,
211 "constantValues": true,
212 "functionTypeParameters": true,
213 "parameterNames": true,
214 "rangeVariableTypes": true
215 }
216 })))
217 }
218
219 async fn label_for_completion(
220 &self,
221 completion: &lsp::CompletionItem,
222 language: &Arc<Language>,
223 ) -> Option<CodeLabel> {
224 let label = &completion.label;
225
226 // Gopls returns nested fields and methods as completions.
227 // To syntax highlight these, combine their final component
228 // with their detail.
229 let name_offset = label.rfind('.').unwrap_or(0);
230
231 match completion.kind.zip(completion.detail.as_ref()) {
232 Some((lsp::CompletionItemKind::MODULE, detail)) => {
233 let text = format!("{label} {detail}");
234 let source = Rope::from(format!("import {text}").as_str());
235 let runs = language.highlight_text(&source, 7..7 + text.len());
236 return Some(CodeLabel {
237 text,
238 runs,
239 filter_range: 0..label.len(),
240 });
241 }
242 Some((
243 lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
244 detail,
245 )) => {
246 let text = format!("{label} {detail}");
247 let source =
248 Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
249 let runs = adjust_runs(
250 name_offset,
251 language.highlight_text(&source, 4..4 + text.len()),
252 );
253 return Some(CodeLabel {
254 text,
255 runs,
256 filter_range: 0..label.len(),
257 });
258 }
259 Some((lsp::CompletionItemKind::STRUCT, _)) => {
260 let text = format!("{label} struct {{}}");
261 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
262 let runs = adjust_runs(
263 name_offset,
264 language.highlight_text(&source, 5..5 + text.len()),
265 );
266 return Some(CodeLabel {
267 text,
268 runs,
269 filter_range: 0..label.len(),
270 });
271 }
272 Some((lsp::CompletionItemKind::INTERFACE, _)) => {
273 let text = format!("{label} interface {{}}");
274 let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
275 let runs = adjust_runs(
276 name_offset,
277 language.highlight_text(&source, 5..5 + text.len()),
278 );
279 return Some(CodeLabel {
280 text,
281 runs,
282 filter_range: 0..label.len(),
283 });
284 }
285 Some((lsp::CompletionItemKind::FIELD, detail)) => {
286 let text = format!("{label} {detail}");
287 let source =
288 Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
289 let runs = adjust_runs(
290 name_offset,
291 language.highlight_text(&source, 16..16 + text.len()),
292 );
293 return Some(CodeLabel {
294 text,
295 runs,
296 filter_range: 0..label.len(),
297 });
298 }
299 Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
300 if let Some(signature) = detail.strip_prefix("func") {
301 let text = format!("{label}{signature}");
302 let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
303 let runs = adjust_runs(
304 name_offset,
305 language.highlight_text(&source, 5..5 + text.len()),
306 );
307 return Some(CodeLabel {
308 filter_range: 0..label.len(),
309 text,
310 runs,
311 });
312 }
313 }
314 _ => {}
315 }
316 None
317 }
318
319 async fn label_for_symbol(
320 &self,
321 name: &str,
322 kind: lsp::SymbolKind,
323 language: &Arc<Language>,
324 ) -> Option<CodeLabel> {
325 let (text, filter_range, display_range) = match kind {
326 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
327 let text = format!("func {} () {{}}", name);
328 let filter_range = 5..5 + name.len();
329 let display_range = 0..filter_range.end;
330 (text, filter_range, display_range)
331 }
332 lsp::SymbolKind::STRUCT => {
333 let text = format!("type {} struct {{}}", name);
334 let filter_range = 5..5 + name.len();
335 let display_range = 0..text.len();
336 (text, filter_range, display_range)
337 }
338 lsp::SymbolKind::INTERFACE => {
339 let text = format!("type {} interface {{}}", name);
340 let filter_range = 5..5 + name.len();
341 let display_range = 0..text.len();
342 (text, filter_range, display_range)
343 }
344 lsp::SymbolKind::CLASS => {
345 let text = format!("type {} T", name);
346 let filter_range = 5..5 + name.len();
347 let display_range = 0..filter_range.end;
348 (text, filter_range, display_range)
349 }
350 lsp::SymbolKind::CONSTANT => {
351 let text = format!("const {} = nil", name);
352 let filter_range = 6..6 + name.len();
353 let display_range = 0..filter_range.end;
354 (text, filter_range, display_range)
355 }
356 lsp::SymbolKind::VARIABLE => {
357 let text = format!("var {} = nil", name);
358 let filter_range = 4..4 + name.len();
359 let display_range = 0..filter_range.end;
360 (text, filter_range, display_range)
361 }
362 lsp::SymbolKind::MODULE => {
363 let text = format!("package {}", name);
364 let filter_range = 8..8 + name.len();
365 let display_range = 0..filter_range.end;
366 (text, filter_range, display_range)
367 }
368 _ => return None,
369 };
370
371 Some(CodeLabel {
372 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
373 text: text[display_range].to_string(),
374 filter_range,
375 })
376 }
377
378 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
379 static REGEX: LazyLock<Regex> =
380 LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));
381 Some(REGEX.replace_all(message, "\n\n").to_string())
382 }
383}
384
385fn parse_version_output(output: &Output) -> Result<&str> {
386 let version_stdout =
387 str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;
388
389 let version = VERSION_REGEX
390 .find(version_stdout)
391 .with_context(|| format!("failed to parse version output '{version_stdout}'"))?
392 .as_str();
393
394 Ok(version)
395}
396
397async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
398 maybe!(async {
399 let mut last_binary_path = None;
400 let mut entries = fs::read_dir(&container_dir).await?;
401 while let Some(entry) = entries.next().await {
402 let entry = entry?;
403 if entry.file_type().await?.is_file()
404 && entry
405 .file_name()
406 .to_str()
407 .map_or(false, |name| name.starts_with("gopls_"))
408 {
409 last_binary_path = Some(entry.path());
410 }
411 }
412
413 let path = last_binary_path.context("no cached binary")?;
414 anyhow::Ok(LanguageServerBinary {
415 path,
416 arguments: server_binary_arguments(),
417 env: None,
418 })
419 })
420 .await
421 .log_err()
422}
423
424fn adjust_runs(
425 delta: usize,
426 mut runs: Vec<(Range<usize>, HighlightId)>,
427) -> Vec<(Range<usize>, HighlightId)> {
428 for (range, _) in &mut runs {
429 range.start += delta;
430 range.end += delta;
431 }
432 runs
433}
434
435pub(crate) struct GoContextProvider;
436
437const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
438const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
439 VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
440const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
441 VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
442
443impl ContextProvider for GoContextProvider {
444 fn build_context(
445 &self,
446 variables: &TaskVariables,
447 location: ContextLocation<'_>,
448 _: Option<HashMap<String, String>>,
449 _: Arc<dyn LanguageToolchainStore>,
450 cx: &mut gpui::App,
451 ) -> Task<Result<TaskVariables>> {
452 let local_abs_path = location
453 .file_location
454 .buffer
455 .read(cx)
456 .file()
457 .and_then(|file| Some(file.as_local()?.abs_path(cx)));
458
459 let go_package_variable = local_abs_path
460 .as_deref()
461 .and_then(|local_abs_path| local_abs_path.parent())
462 .map(|buffer_dir| {
463 // Prefer the relative form `./my-nested-package/is-here` over
464 // absolute path, because it's more readable in the modal, but
465 // the absolute path also works.
466 let package_name = variables
467 .get(&VariableName::WorktreeRoot)
468 .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
469 .map(|relative_pkg_dir| {
470 if relative_pkg_dir.as_os_str().is_empty() {
471 ".".into()
472 } else {
473 format!("./{}", relative_pkg_dir.to_string_lossy())
474 }
475 })
476 .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
477
478 (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
479 });
480
481 let go_module_root_variable = local_abs_path
482 .as_deref()
483 .and_then(|local_abs_path| local_abs_path.parent())
484 .map(|buffer_dir| {
485 // Walk dirtree up until getting the first go.mod file
486 let module_dir = buffer_dir
487 .ancestors()
488 .find(|dir| dir.join("go.mod").is_file())
489 .map(|dir| dir.to_string_lossy().to_string())
490 .unwrap_or_else(|| ".".to_string());
491
492 (GO_MODULE_ROOT_TASK_VARIABLE.clone(), module_dir)
493 });
494
495 let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
496
497 let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
498 .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
499
500 Task::ready(Ok(TaskVariables::from_iter(
501 [
502 go_package_variable,
503 go_subtest_variable,
504 go_module_root_variable,
505 ]
506 .into_iter()
507 .flatten(),
508 )))
509 }
510
511 fn associated_tasks(
512 &self,
513 _: Arc<dyn Fs>,
514 _: Option<Arc<dyn File>>,
515 _: &App,
516 ) -> Task<Option<TaskTemplates>> {
517 let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
518 None
519 } else {
520 Some("$ZED_DIRNAME".to_string())
521 };
522 let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
523
524 Task::ready(Some(TaskTemplates(vec![
525 TaskTemplate {
526 label: format!(
527 "go test {} -run {}",
528 GO_PACKAGE_TASK_VARIABLE.template_value(),
529 VariableName::Symbol.template_value(),
530 ),
531 command: "go".into(),
532 args: vec![
533 "test".into(),
534 "-run".into(),
535 format!("\\^{}\\$", VariableName::Symbol.template_value(),),
536 ],
537 tags: vec!["go-test".to_owned()],
538 cwd: package_cwd.clone(),
539 ..TaskTemplate::default()
540 },
541 TaskTemplate {
542 label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
543 command: "go".into(),
544 args: vec!["test".into()],
545 cwd: package_cwd.clone(),
546 ..TaskTemplate::default()
547 },
548 TaskTemplate {
549 label: "go test ./...".into(),
550 command: "go".into(),
551 args: vec!["test".into(), "./...".into()],
552 cwd: module_cwd.clone(),
553 ..TaskTemplate::default()
554 },
555 TaskTemplate {
556 label: format!(
557 "go test {} -v -run {}/{}",
558 GO_PACKAGE_TASK_VARIABLE.template_value(),
559 VariableName::Symbol.template_value(),
560 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
561 ),
562 command: "go".into(),
563 args: vec![
564 "test".into(),
565 "-v".into(),
566 "-run".into(),
567 format!(
568 "\\^{}\\$/\\^{}\\$",
569 VariableName::Symbol.template_value(),
570 GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
571 ),
572 ],
573 cwd: package_cwd.clone(),
574 tags: vec!["go-subtest".to_owned()],
575 ..TaskTemplate::default()
576 },
577 TaskTemplate {
578 label: format!(
579 "go test {} -bench {}",
580 GO_PACKAGE_TASK_VARIABLE.template_value(),
581 VariableName::Symbol.template_value()
582 ),
583 command: "go".into(),
584 args: vec![
585 "test".into(),
586 "-benchmem".into(),
587 "-run='^$'".into(),
588 "-bench".into(),
589 format!("\\^{}\\$", VariableName::Symbol.template_value()),
590 ],
591 cwd: package_cwd.clone(),
592 tags: vec!["go-benchmark".to_owned()],
593 ..TaskTemplate::default()
594 },
595 TaskTemplate {
596 label: format!(
597 "go test {} -fuzz=Fuzz -run {}",
598 GO_PACKAGE_TASK_VARIABLE.template_value(),
599 VariableName::Symbol.template_value(),
600 ),
601 command: "go".into(),
602 args: vec![
603 "test".into(),
604 "-fuzz=Fuzz".into(),
605 "-run".into(),
606 format!("\\^{}\\$", VariableName::Symbol.template_value(),),
607 ],
608 tags: vec!["go-fuzz".to_owned()],
609 cwd: package_cwd.clone(),
610 ..TaskTemplate::default()
611 },
612 TaskTemplate {
613 label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
614 command: "go".into(),
615 args: vec!["run".into(), ".".into()],
616 cwd: package_cwd.clone(),
617 tags: vec!["go-main".to_owned()],
618 ..TaskTemplate::default()
619 },
620 TaskTemplate {
621 label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
622 command: "go".into(),
623 args: vec!["generate".into()],
624 cwd: package_cwd.clone(),
625 tags: vec!["go-generate".to_owned()],
626 ..TaskTemplate::default()
627 },
628 TaskTemplate {
629 label: "go generate ./...".into(),
630 command: "go".into(),
631 args: vec!["generate".into(), "./...".into()],
632 cwd: module_cwd.clone(),
633 ..TaskTemplate::default()
634 },
635 ])))
636 }
637}
638
639fn extract_subtest_name(input: &str) -> Option<String> {
640 let replaced_spaces = input.trim_matches('"').replace(' ', "_");
641
642 Some(
643 GO_ESCAPE_SUBTEST_NAME_REGEX
644 .replace_all(&replaced_spaces, |caps: ®ex::Captures| {
645 format!("\\{}", &caps[0])
646 })
647 .to_string(),
648 )
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::language;
655 use gpui::Hsla;
656 use theme::SyntaxTheme;
657
658 #[gpui::test]
659 async fn test_go_label_for_completion() {
660 let adapter = Arc::new(GoLspAdapter);
661 let language = language("go", tree_sitter_go::LANGUAGE.into());
662
663 let theme = SyntaxTheme::new_test([
664 ("type", Hsla::default()),
665 ("keyword", Hsla::default()),
666 ("function", Hsla::default()),
667 ("number", Hsla::default()),
668 ("property", Hsla::default()),
669 ]);
670 language.set_theme(&theme);
671
672 let grammar = language.grammar().unwrap();
673 let highlight_function = grammar.highlight_id_for_name("function").unwrap();
674 let highlight_type = grammar.highlight_id_for_name("type").unwrap();
675 let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
676 let highlight_number = grammar.highlight_id_for_name("number").unwrap();
677
678 assert_eq!(
679 adapter
680 .label_for_completion(
681 &lsp::CompletionItem {
682 kind: Some(lsp::CompletionItemKind::FUNCTION),
683 label: "Hello".to_string(),
684 detail: Some("func(a B) c.D".to_string()),
685 ..Default::default()
686 },
687 &language
688 )
689 .await,
690 Some(CodeLabel {
691 text: "Hello(a B) c.D".to_string(),
692 filter_range: 0..5,
693 runs: vec![
694 (0..5, highlight_function),
695 (8..9, highlight_type),
696 (13..14, highlight_type),
697 ],
698 })
699 );
700
701 // Nested methods
702 assert_eq!(
703 adapter
704 .label_for_completion(
705 &lsp::CompletionItem {
706 kind: Some(lsp::CompletionItemKind::METHOD),
707 label: "one.two.Three".to_string(),
708 detail: Some("func() [3]interface{}".to_string()),
709 ..Default::default()
710 },
711 &language
712 )
713 .await,
714 Some(CodeLabel {
715 text: "one.two.Three() [3]interface{}".to_string(),
716 filter_range: 0..13,
717 runs: vec![
718 (8..13, highlight_function),
719 (17..18, highlight_number),
720 (19..28, highlight_keyword),
721 ],
722 })
723 );
724
725 // Nested fields
726 assert_eq!(
727 adapter
728 .label_for_completion(
729 &lsp::CompletionItem {
730 kind: Some(lsp::CompletionItemKind::FIELD),
731 label: "two.Three".to_string(),
732 detail: Some("a.Bcd".to_string()),
733 ..Default::default()
734 },
735 &language
736 )
737 .await,
738 Some(CodeLabel {
739 text: "two.Three a.Bcd".to_string(),
740 filter_range: 0..9,
741 runs: vec![(12..15, highlight_type)],
742 })
743 );
744 }
745}