1use std::collections::HashMap;
21use std::collections::HashSet;
22use std::fmt::Debug;
23use std::path::Path;
24use std::path::PathBuf;
25use std::sync::Arc;
26use std::sync::RwLock;
27
28use derivative::Derivative;
29use derive_more::Display;
30use dupe::Dupe;
31use dupe::OptionDupedExt;
32use itertools::Itertools;
33use lsp_server::Connection;
34use lsp_server::Message;
35use lsp_server::Notification;
36use lsp_server::Request;
37use lsp_server::RequestId;
38use lsp_server::Response;
39use lsp_server::ResponseError;
40use lsp_types::notification::DidChangeTextDocument;
41use lsp_types::notification::DidCloseTextDocument;
42use lsp_types::notification::DidOpenTextDocument;
43use lsp_types::notification::LogMessage;
44use lsp_types::notification::PublishDiagnostics;
45use lsp_types::request::Completion;
46use lsp_types::request::GotoDefinition;
47use lsp_types::request::HoverRequest;
48use lsp_types::CompletionItem;
49use lsp_types::CompletionItemKind;
50use lsp_types::CompletionOptions;
51use lsp_types::CompletionParams;
52use lsp_types::CompletionResponse;
53use lsp_types::DefinitionOptions;
54use lsp_types::Diagnostic;
55use lsp_types::DidChangeTextDocumentParams;
56use lsp_types::DidCloseTextDocumentParams;
57use lsp_types::DidOpenTextDocumentParams;
58use lsp_types::Documentation;
59use lsp_types::GotoDefinitionParams;
60use lsp_types::GotoDefinitionResponse;
61use lsp_types::Hover;
62use lsp_types::HoverContents;
63use lsp_types::HoverParams;
64use lsp_types::HoverProviderCapability;
65use lsp_types::InitializeParams;
66use lsp_types::LanguageString;
67use lsp_types::LocationLink;
68use lsp_types::LogMessageParams;
69use lsp_types::MarkedString;
70use lsp_types::MarkupContent;
71use lsp_types::MarkupKind;
72use lsp_types::MessageType;
73use lsp_types::OneOf;
74use lsp_types::Position;
75use lsp_types::PublishDiagnosticsParams;
76use lsp_types::Range;
77use lsp_types::ServerCapabilities;
78use lsp_types::TextDocumentSyncCapability;
79use lsp_types::TextDocumentSyncKind;
80use lsp_types::TextEdit;
81use lsp_types::Url;
82use lsp_types::WorkDoneProgressOptions;
83use lsp_types::WorkspaceFolder;
84use serde::de::DeserializeOwned;
85use serde::Deserialize;
86use serde::Deserializer;
87use serde::Serialize;
88use serde::Serializer;
89use starlark::codemap::ResolvedSpan;
90use starlark::codemap::Span;
91use starlark::docs::markdown::render_doc_item_no_link;
92use starlark::docs::markdown::render_doc_param;
93use starlark::docs::DocItem;
94use starlark::docs::DocMember;
95use starlark::docs::DocModule;
96use starlark::syntax::AstModule;
97use starlark_syntax::codemap::ResolvedPos;
98use starlark_syntax::syntax::ast::AstPayload;
99use starlark_syntax::syntax::ast::LoadArgP;
100use starlark_syntax::syntax::module::AstModuleFields;
101
102use crate::completion::StringCompletionResult;
103use crate::completion::StringCompletionType;
104use crate::definition::Definition;
105use crate::definition::DottedDefinition;
106use crate::definition::IdentifierDefinition;
107use crate::definition::LspModule;
108use crate::inspect::AstModuleInspect;
109use crate::inspect::AutocompleteType;
110use crate::symbols::find_symbols_at_location;
111
112struct StarlarkFileContentsRequest {}
114
115impl lsp_types::request::Request for StarlarkFileContentsRequest {
116 type Params = StarlarkFileContentsParams;
117 type Result = StarlarkFileContentsResponse;
118 const METHOD: &'static str = "starlark/fileContents";
119}
120
121#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
123#[serde(rename_all = "camelCase")] struct StarlarkFileContentsParams {
125 uri: LspUrl,
126}
127
128#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
130#[serde(rename_all = "camelCase")] struct StarlarkFileContentsResponse {
132 contents: Option<String>,
133}
134
135#[derive(thiserror::Error, Debug)]
137pub enum LspUrlError {
138 #[error("`{}` does not have an absolute path component", .0)]
141 NotAbsolute(Url),
142 #[error("`{}` could not be converted back to a URL", .0)]
144 Unparsable(LspUrl),
145 #[error("invalid URL for file:// schema (possibly not absolute?): `{}`", .0)]
146 InvalidFileUrl(Url),
147}
148
149#[derive(Clone, Debug, Hash, Eq, PartialEq, Display)]
151pub enum LspUrl {
152 #[display("file://{}", _0.display())]
154 File(PathBuf),
155 #[display("starlark:{}", _0.display())]
158 Starlark(PathBuf),
159 #[display("{}", _0)]
161 Other(Url),
162}
163
164impl Serialize for LspUrl {
165 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 match Url::try_from(self) {
170 Ok(url) => url.serialize(serializer),
171 Err(e) => Err(serde::ser::Error::custom(e.to_string())),
172 }
173 }
174}
175
176impl<'de> Deserialize<'de> for LspUrl {
177 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178 where
179 D: Deserializer<'de>,
180 {
181 let url = Url::deserialize(deserializer)?;
182 LspUrl::try_from(url).map_err(|e| serde::de::Error::custom(e.to_string()))
183 }
184}
185
186impl LspUrl {
187 pub fn path(&self) -> &Path {
189 match self {
190 LspUrl::File(p) => p.as_path(),
191 LspUrl::Starlark(p) => p.as_path(),
192 LspUrl::Other(u) => Path::new(u.path()),
193 }
194 }
195}
196
197impl TryFrom<Url> for LspUrl {
198 type Error = LspUrlError;
199
200 fn try_from(url: Url) -> Result<Self, Self::Error> {
201 match url.scheme() {
202 "file" => {
203 let file_path = PathBuf::from(
204 url.to_file_path()
205 .map_err(|_| LspUrlError::InvalidFileUrl(url.clone()))?,
206 );
207 if file_path.is_absolute() {
208 Ok(Self::File(file_path))
209 } else {
210 Err(LspUrlError::NotAbsolute(url))
211 }
212 }
213 "starlark" => {
214 let path = PathBuf::from(url.path());
215 if path.to_string_lossy().starts_with('/') {
218 Ok(Self::Starlark(path))
219 } else {
220 Err(LspUrlError::NotAbsolute(url))
221 }
222 }
223 _ => Ok(Self::Other(url)),
224 }
225 }
226}
227
228impl TryFrom<LspUrl> for Url {
229 type Error = LspUrlError;
230
231 fn try_from(url: LspUrl) -> Result<Self, Self::Error> {
232 Url::try_from(&url)
233 }
234}
235
236impl TryFrom<&LspUrl> for Url {
237 type Error = LspUrlError;
238
239 fn try_from(url: &LspUrl) -> Result<Self, Self::Error> {
240 match &url {
241 LspUrl::File(p) => {
242 Url::from_file_path(p).map_err(|_| LspUrlError::Unparsable(url.clone()))
243 }
244 LspUrl::Starlark(p) => Url::parse(&format!("starlark:{}", p.display()))
245 .map_err(|_| LspUrlError::Unparsable(url.clone())),
246 LspUrl::Other(u) => Ok(u.clone()),
247 }
248 }
249}
250
251#[derive(Derivative)]
253#[derivative(Debug)]
254pub struct StringLiteralResult {
255 pub url: LspUrl,
257 #[derivative(Debug = "ignore")]
262 pub location_finder: Option<Box<dyn FnOnce(&AstModule) -> anyhow::Result<Option<Span>> + Send>>,
263}
264
265fn _assert_string_literal_result_is_send() {
266 fn assert_send<T: Send>() {}
267 assert_send::<StringLiteralResult>();
268}
269
270#[derive(Default)]
272pub struct LspEvalResult {
273 pub diagnostics: Vec<Diagnostic>,
275 pub ast: Option<AstModule>,
277}
278
279#[derive(Dupe, Clone, Debug, serde::Serialize, serde::Deserialize)]
282pub struct LspServerSettings {
283 pub enable_goto_definition: bool,
285}
286
287impl Default for LspServerSettings {
288 fn default() -> Self {
289 Self {
290 enable_goto_definition: true,
291 }
292 }
293}
294
295pub trait LspContext {
297 fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult;
299
300 fn resolve_load(
307 &self,
308 path: &str,
309 current_file: &LspUrl,
310 workspace_root: Option<&Path>,
311 ) -> anyhow::Result<LspUrl>;
312
313 fn render_as_load(
319 &self,
320 target: &LspUrl,
321 current_file: &LspUrl,
322 workspace_root: Option<&Path>,
323 ) -> anyhow::Result<String>;
324
325 fn resolve_string_literal(
332 &self,
333 literal: &str,
334 current_file: &LspUrl,
335 workspace_root: Option<&Path>,
336 ) -> anyhow::Result<Option<StringLiteralResult>>;
337
338 fn get_load_contents(&self, uri: &LspUrl) -> anyhow::Result<Option<String>>;
340
341 fn parse_file(&self, uri: &LspUrl) -> anyhow::Result<Option<LspEvalResult>> {
343 let result = self
344 .get_load_contents(uri)?
345 .map(|content| self.parse_file_with_contents(uri, content));
346 Ok(result)
347 }
348
349 fn get_environment(&self, uri: &LspUrl) -> DocModule;
351
352 fn get_url_for_global_symbol(
357 &self,
358 current_file: &LspUrl,
359 symbol: &str,
360 ) -> anyhow::Result<Option<LspUrl>>;
361
362 fn get_string_completion_options(
366 &self,
367 document_uri: &LspUrl,
368 kind: StringCompletionType,
369 current_value: &str,
370 workspace_root: Option<&Path>,
371 ) -> anyhow::Result<Vec<StringCompletionResult>> {
372 let _unused = (document_uri, kind, current_value, workspace_root);
373 Ok(Vec::new())
374 }
375}
376
377#[derive(thiserror::Error, Debug)]
379enum ResolveLoadError {
380 #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
382 WrongScheme(String, LspUrl),
383}
384
385#[derive(thiserror::Error, Debug)]
387pub(crate) enum LoadContentsError {
388 #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
390 WrongScheme(String, LspUrl),
391}
392
393pub(crate) struct Backend<T: LspContext> {
394 connection: Connection,
395 pub(crate) context: T,
396 pub(crate) last_valid_parse: RwLock<HashMap<LspUrl, Arc<LspModule>>>,
399}
400
401impl<T: LspContext> Backend<T> {
403 fn server_capabilities(settings: LspServerSettings) -> ServerCapabilities {
404 let definition_provider = settings.enable_goto_definition.then_some({
405 OneOf::Right(DefinitionOptions {
406 work_done_progress_options: WorkDoneProgressOptions {
407 work_done_progress: None,
408 },
409 })
410 });
411 ServerCapabilities {
412 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
413 definition_provider,
414 completion_provider: Some(CompletionOptions::default()),
415 hover_provider: Some(HoverProviderCapability::Simple(true)),
416 ..ServerCapabilities::default()
417 }
418 }
419
420 fn get_ast(&self, uri: &LspUrl) -> Option<Arc<LspModule>> {
421 let last_valid_parse = self.last_valid_parse.read().unwrap();
422 last_valid_parse.get(uri).duped()
423 }
424
425 pub(crate) fn get_ast_or_load_from_disk(
426 &self,
427 uri: &LspUrl,
428 ) -> anyhow::Result<Option<Arc<LspModule>>> {
429 let module = match self.get_ast(uri) {
430 Some(result) => Some(result),
431 None => self
432 .context
433 .parse_file(uri)?
434 .and_then(|eval_result| eval_result.ast.map(|ast| Arc::new(LspModule::new(ast)))),
435 };
436 Ok(module)
437 }
438
439 fn validate(&self, uri: Url, version: Option<i64>, text: String) -> anyhow::Result<()> {
440 let lsp_url = uri.clone().try_into()?;
441 let eval_result = self.context.parse_file_with_contents(&lsp_url, text);
442 if let Some(ast) = eval_result.ast {
443 let module = Arc::new(LspModule::new(ast));
444 let mut last_valid_parse = self.last_valid_parse.write().unwrap();
445 last_valid_parse.insert(lsp_url, module);
446 }
447 self.publish_diagnostics(uri, eval_result.diagnostics, version);
448 Ok(())
449 }
450
451 fn did_open(&self, params: DidOpenTextDocumentParams) -> anyhow::Result<()> {
452 self.validate(
453 params.text_document.uri,
454 Some(params.text_document.version as i64),
455 params.text_document.text,
456 )
457 }
458
459 fn did_change(&self, params: DidChangeTextDocumentParams) -> anyhow::Result<()> {
460 let change = params.content_changes.into_iter().next().unwrap();
462 self.validate(
463 params.text_document.uri,
464 Some(params.text_document.version as i64),
465 change.text,
466 )
467 }
468
469 fn did_close(&self, params: DidCloseTextDocumentParams) -> anyhow::Result<()> {
470 {
471 let mut last_valid_parse = self.last_valid_parse.write().unwrap();
472 last_valid_parse.remove(¶ms.text_document.uri.clone().try_into()?);
473 }
474 self.publish_diagnostics(params.text_document.uri, Vec::new(), None);
475 Ok(())
476 }
477
478 fn goto_definition(
485 &self,
486 id: RequestId,
487 params: GotoDefinitionParams,
488 initialize_params: &InitializeParams,
489 ) {
490 self.send_response(new_response(
491 id,
492 self.find_definition(params, initialize_params),
493 ));
494 }
495
496 fn completion(
498 &self,
499 id: RequestId,
500 params: CompletionParams,
501 initialize_params: &InitializeParams,
502 ) {
503 self.send_response(new_response(
504 id,
505 self.completion_options(params, initialize_params),
506 ));
507 }
508
509 fn hover(&self, id: RequestId, params: HoverParams, initialize_params: &InitializeParams) {
511 self.send_response(new_response(id, self.hover_info(params, initialize_params)));
512 }
513
514 fn get_starlark_file_contents(&self, id: RequestId, params: StarlarkFileContentsParams) {
516 let response: anyhow::Result<_> = match params.uri {
517 LspUrl::Starlark(_) => self
518 .context
519 .get_load_contents(¶ms.uri)
520 .map(|contents| StarlarkFileContentsResponse { contents }),
521 _ => Err(LoadContentsError::WrongScheme("starlark:".to_owned(), params.uri).into()),
522 };
523 self.send_response(new_response(id, response));
524 }
525
526 pub(crate) fn resolve_load_path(
527 &self,
528 path: &str,
529 current_uri: &LspUrl,
530 workspace_root: Option<&Path>,
531 ) -> anyhow::Result<LspUrl> {
532 match current_uri {
533 LspUrl::File(_) => self.context.resolve_load(path, current_uri, workspace_root),
534 LspUrl::Starlark(_) | LspUrl::Other(_) => {
535 Err(ResolveLoadError::WrongScheme("file://".to_owned(), current_uri.clone()).into())
536 }
537 }
538 }
539
540 fn location_link<R: Into<Range> + Copy>(
542 source: ResolvedSpan,
543 uri: &LspUrl,
544 target_range: R,
545 ) -> anyhow::Result<Option<LocationLink>> {
546 Ok(Some(LocationLink {
547 origin_selection_range: Some(source.into()),
548 target_uri: uri.try_into()?,
549 target_range: target_range.into(),
550 target_selection_range: target_range.into(),
551 }))
552 }
553
554 fn resolve_definition_location(
561 &self,
562 definition: IdentifierDefinition,
563 source: ResolvedSpan,
564 member: Option<&str>,
565 uri: &LspUrl,
566 workspace_root: Option<&Path>,
567 ) -> anyhow::Result<Option<LocationLink>> {
568 let ret = match definition {
569 IdentifierDefinition::Location {
570 destination: target,
571 ..
572 } => Self::location_link(source, uri, target)?,
573 IdentifierDefinition::LoadedLocation {
574 destination: location,
575 path,
576 name,
577 ..
578 } => {
579 let load_uri = self.resolve_load_path(&path, uri, workspace_root)?;
580 let loaded_location =
581 self.get_ast_or_load_from_disk(&load_uri)?
582 .and_then(|ast| match member {
583 Some(member) => ast.find_exported_symbol_and_member(&name, member),
584 None => ast.find_exported_symbol_span(&name),
585 });
586 match loaded_location {
587 None => Self::location_link(source, uri, location)?,
588 Some(loaded_location) => {
589 Self::location_link(source, &load_uri, loaded_location)?
590 }
591 }
592 }
593 IdentifierDefinition::NotFound => None,
594 IdentifierDefinition::LoadPath { path, .. } => {
595 match self.resolve_load_path(&path, uri, workspace_root) {
596 Ok(load_uri) => Self::location_link(source, &load_uri, Range::default())?,
597 Err(_) => None,
598 }
599 }
600 IdentifierDefinition::StringLiteral { literal, .. } => {
601 let Ok(resolved_literal) =
602 self.context
603 .resolve_string_literal(&literal, uri, workspace_root)
604 else {
605 return Ok(None);
606 };
607 match resolved_literal {
608 Some(StringLiteralResult {
609 url,
610 location_finder: Some(location_finder),
611 }) => {
612 let result =
615 self.get_ast_or_load_from_disk(&url)
616 .and_then(|ast| match ast {
617 Some(module) => location_finder(&module.ast).map(|span| {
618 span.map(|span| module.ast.codemap().resolve_span(span))
619 }),
620 None => Ok(None),
621 });
622 let result = match result {
623 Ok(result) => result,
624 Err(e) => {
625 eprintln!("Error jumping to definition: {:#}", e);
626 None
627 }
628 };
629 let target_range = result.unwrap_or_default();
630 Self::location_link(source, &url, target_range)?
631 }
632 Some(StringLiteralResult {
633 url,
634 location_finder: None,
635 }) => Self::location_link(source, &url, Range::default())?,
636 _ => None,
637 }
638 }
639 IdentifierDefinition::Unresolved { name, .. } => {
640 match self.context.get_url_for_global_symbol(uri, &name)? {
641 Some(uri) => {
642 let loaded_location =
643 self.get_ast_or_load_from_disk(&uri)?
644 .and_then(|ast| match member {
645 Some(member) => {
646 ast.find_exported_symbol_and_member(&name, member)
647 }
648 None => ast.find_exported_symbol_span(&name),
649 });
650
651 Self::location_link(source, &uri, loaded_location.unwrap_or_default())?
652 }
653 None => None,
654 }
655 }
656 };
657 Ok(ret)
658 }
659
660 fn find_definition(
661 &self,
662 params: GotoDefinitionParams,
663 initialize_params: &InitializeParams,
664 ) -> anyhow::Result<GotoDefinitionResponse> {
665 let uri = params
666 .text_document_position_params
667 .text_document
668 .uri
669 .try_into()?;
670 let line = params.text_document_position_params.position.line;
671 let character = params.text_document_position_params.position.character;
672 let workspace_root =
673 Self::get_workspace_root(initialize_params.workspace_folders.as_ref(), &uri);
674
675 let location = match self.get_ast(&uri) {
676 Some(ast) => {
677 let location = ast.find_definition_at_location(line, character);
678 let source = location.source().unwrap_or_default();
679 match location {
680 Definition::Identifier(definition) => self.resolve_definition_location(
681 definition,
682 source,
683 None,
684 &uri,
685 workspace_root.as_deref(),
686 )?,
687 Definition::Dotted(DottedDefinition {
691 root_definition_location: IdentifierDefinition::Location { destination, .. },
692 segments,
693 ..
694 }) => {
695 let member_location = ast
696 .find_exported_symbol_and_member(
697 segments.first().expect("at least one segment").as_str(),
698 segments.get(1).expect("at least two segments").as_str(),
699 )
700 .unwrap_or(destination);
701 Self::location_link(source, &uri, member_location)?
702 }
703 Definition::Dotted(definition) => self.resolve_definition_location(
704 definition.root_definition_location,
705 source,
706 Some(
707 definition
708 .segments
709 .last()
710 .expect("to have at least one component")
711 .as_str(),
712 ),
713 &uri,
714 workspace_root.as_deref(),
715 )?,
716 }
717 }
718 None => None,
719 };
720
721 let response = match location {
722 Some(location) => vec![location],
723 None => vec![],
724 };
725 Ok(GotoDefinitionResponse::Link(response))
726 }
727
728 fn completion_options(
729 &self,
730 params: CompletionParams,
731 initialize_params: &InitializeParams,
732 ) -> anyhow::Result<CompletionResponse> {
733 let uri = params.text_document_position.text_document.uri.try_into()?;
734 let line = params.text_document_position.position.line;
735 let character = params.text_document_position.position.character;
736
737 let symbols: Option<Vec<_>> = match self.get_ast(&uri) {
738 Some(document) => {
739 let autocomplete_type = document.ast.get_auto_complete_type(line, character);
742 let workspace_root =
743 Self::get_workspace_root(initialize_params.workspace_folders.as_ref(), &uri);
744
745 match &autocomplete_type {
746 None | Some(AutocompleteType::None) => None,
747 Some(AutocompleteType::Default) => Some(
748 self.default_completion_options(
749 &uri,
750 &document,
751 line,
752 character,
753 workspace_root.as_deref(),
754 )
755 .collect(),
756 ),
757 Some(AutocompleteType::LoadPath {
758 current_value,
759 current_span,
760 })
761 | Some(AutocompleteType::String {
762 current_value,
763 current_span,
764 }) => Some(self.string_completion_options(
765 &uri,
766 if matches!(&autocomplete_type, Some(AutocompleteType::LoadPath { .. })) {
767 StringCompletionType::LoadPath
768 } else {
769 StringCompletionType::String
770 },
771 current_value,
772 *current_span,
773 workspace_root.as_deref(),
774 )?),
775 Some(AutocompleteType::LoadSymbol {
776 path,
777 current_span,
778 previously_loaded,
779 }) => Some(self.exported_symbol_options(
780 path,
781 *current_span,
782 previously_loaded,
783 &uri,
784 workspace_root.as_deref(),
785 )),
786 Some(AutocompleteType::Parameter {
787 function_name_span,
788 previously_used_named_parameters,
789 ..
790 }) => Some(
791 self.parameter_name_options(
792 function_name_span,
793 &document,
794 &uri,
795 previously_used_named_parameters,
796 workspace_root.as_deref(),
797 )
798 .chain(self.default_completion_options(
799 &uri,
800 &document,
801 line,
802 character,
803 workspace_root.as_deref(),
804 ))
805 .collect(),
806 ),
807 Some(AutocompleteType::Type) => Some(Self::type_completion_options().collect()),
808 }
809 }
810 None => None,
811 };
812
813 Ok(CompletionResponse::Array(symbols.unwrap_or_default()))
814 }
815
816 pub(crate) fn get_all_exported_symbols<F, S>(
821 &self,
822 except_from: Option<&LspUrl>,
823 symbols: &HashMap<String, S>,
824 workspace_root: Option<&Path>,
825 current_document: &LspUrl,
826 format_text_edit: F,
827 ) -> Vec<CompletionItem>
828 where
829 F: Fn(&str, &str) -> TextEdit,
830 {
831 let mut seen = HashSet::new();
832 let mut result = Vec::new();
833
834 let all_documents = self.last_valid_parse.read().unwrap();
835
836 for (doc_uri, doc) in all_documents
837 .iter()
838 .filter(|&(doc_uri, _)| match except_from {
839 Some(uri) => doc_uri != uri,
840 None => true,
841 })
842 {
843 let Ok(load_path) =
844 self.context
845 .render_as_load(doc_uri, current_document, workspace_root)
846 else {
847 continue;
848 };
849
850 for symbol in doc
851 .get_exported_symbols()
852 .into_iter()
853 .filter(|symbol| !symbols.contains_key(&symbol.name))
854 {
855 seen.insert(format!("{load_path}:{}", &symbol.name));
856
857 let text_edits = Some(vec![format_text_edit(&load_path, &symbol.name)]);
858 let mut completion_item: CompletionItem = symbol.into();
859 completion_item.detail = Some(format!("Load from {load_path}"));
860 completion_item.additional_text_edits = text_edits;
861
862 result.push(completion_item)
863 }
864 }
865
866 for (doc_uri, symbol) in all_documents
867 .iter()
868 .filter(|&(doc_uri, _)| match except_from {
869 Some(uri) => doc_uri != uri,
870 None => true,
871 })
872 .flat_map(|(doc_uri, doc)| {
873 doc.get_loaded_symbols()
874 .into_iter()
875 .map(move |symbol| (doc_uri, symbol))
876 })
877 .filter(|(_, symbol)| !symbols.contains_key(symbol.name))
878 {
879 let Ok(url) = self
880 .context
881 .resolve_load(symbol.loaded_from, doc_uri, workspace_root)
882 else {
883 continue;
884 };
885 let Ok(load_path) = self
886 .context
887 .render_as_load(&url, current_document, workspace_root)
888 else {
889 continue;
890 };
891
892 if seen.insert(format!("{}:{}", &load_path, symbol.name)) {
893 result.push(CompletionItem {
894 label: symbol.name.to_owned(),
895 detail: Some(format!("Load from {}", &load_path)),
896 kind: Some(CompletionItemKind::CONSTANT),
897 additional_text_edits: Some(vec![format_text_edit(&load_path, symbol.name)]),
898 ..Default::default()
899 })
900 }
901 }
902
903 result
904 }
905
906 pub(crate) fn get_global_symbol_completion_items(
907 &self,
908 current_document: &LspUrl,
909 ) -> impl Iterator<Item = CompletionItem> + '_ {
910 self.context
911 .get_environment(current_document)
912 .members
913 .into_iter()
914 .map(|(symbol, documentation)| CompletionItem {
915 label: symbol.clone(),
916 kind: Some(match &documentation {
917 DocItem::Member(DocMember::Function { .. }) => CompletionItemKind::FUNCTION,
918 _ => CompletionItemKind::CONSTANT,
919 }),
920 detail: documentation.get_doc_summary().map(|str| str.to_owned()),
921 documentation: Some(Documentation::MarkupContent(MarkupContent {
922 kind: MarkupKind::Markdown,
923 value: render_doc_item_no_link(&symbol, &documentation),
924 })),
925 ..Default::default()
926 })
927 }
928
929 pub(crate) fn get_load_text_edit<P>(
930 module: &str,
931 symbol: &str,
932 ast: &LspModule,
933 last_load: Option<ResolvedSpan>,
934 existing_load: Option<&(Vec<LoadArgP<P>>, Span)>,
935 ) -> TextEdit
936 where
937 P: AstPayload,
938 {
939 match existing_load {
940 Some((previously_loaded_symbols, load_span)) => {
941 let load_span = ast.ast.codemap().resolve_span(*load_span);
944 let mut load_args: Vec<(&str, &str)> = previously_loaded_symbols
945 .iter()
946 .map(|LoadArgP { local, their, .. }| {
947 (local.ident.as_str(), their.node.as_str())
948 })
949 .collect();
950 load_args.push((symbol, symbol));
951 load_args.sort_by(|(_, a), (_, b)| a.cmp(b));
952
953 TextEdit::new(
954 load_span.into(),
955 format!(
956 "load(\"{module}\", {})",
957 load_args
958 .into_iter()
959 .map(|(assign, import)| {
960 if assign == import {
961 format!("\"{}\"", import)
962 } else {
963 format!("{} = \"{}\"", assign, import)
964 }
965 })
966 .join(", ")
967 ),
968 )
969 }
970 None => {
971 TextEdit::new(
974 match last_load {
975 Some(span) => Range::new(
976 Position::new(span.end.line as u32, span.end.column as u32),
977 Position::new(span.end.line as u32, span.end.column as u32),
978 ),
979 None => Range::new(Position::new(0, 0), Position::new(0, 0)),
980 },
981 format!(
982 "{}load(\"{module}\", \"{symbol}\"){}",
983 if last_load.is_some() { "\n" } else { "" },
984 if last_load.is_some() { "" } else { "\n\n" },
985 ),
986 )
987 }
988 }
989 }
990
991 pub(crate) fn get_keyword_completion_items() -> impl Iterator<Item = CompletionItem> {
993 [
994 "and", "else", "load", "break", "for", "not", "continue", "if", "or", "def", "in",
996 "pass", "elif", "return", "lambda",
997 ]
998 .into_iter()
999 .map(|keyword| CompletionItem {
1000 label: keyword.to_owned(),
1001 kind: Some(CompletionItemKind::KEYWORD),
1002 ..Default::default()
1003 })
1004 }
1005
1006 fn hover_info(
1008 &self,
1009 params: HoverParams,
1010 initialize_params: &InitializeParams,
1011 ) -> anyhow::Result<Hover> {
1012 let uri = params
1013 .text_document_position_params
1014 .text_document
1015 .uri
1016 .try_into()?;
1017 let line = params.text_document_position_params.position.line;
1018 let character = params.text_document_position_params.position.character;
1019 let workspace_root =
1020 Self::get_workspace_root(initialize_params.workspace_folders.as_ref(), &uri);
1021
1022 let not_found = Hover {
1024 contents: HoverContents::Array(vec![]),
1025 range: None,
1026 };
1027
1028 Ok(match self.get_ast(&uri) {
1029 Some(document) => {
1030 let location = document.find_definition_at_location(line, character);
1031 match location {
1032 Definition::Identifier(identifier_definition) => self
1033 .get_hover_for_identifier_definition(
1034 identifier_definition,
1035 &document,
1036 &uri,
1037 workspace_root.as_deref(),
1038 )?,
1039 Definition::Dotted(DottedDefinition {
1040 root_definition_location,
1041 ..
1042 }) => {
1043 self.get_hover_for_identifier_definition(
1046 root_definition_location,
1047 &document,
1048 &uri,
1049 workspace_root.as_deref(),
1050 )?
1051 }
1052 }
1053 .unwrap_or(not_found)
1054 }
1055 None => not_found,
1056 })
1057 }
1058
1059 fn get_hover_for_identifier_definition(
1060 &self,
1061 identifier_definition: IdentifierDefinition,
1062 document: &LspModule,
1063 document_uri: &LspUrl,
1064 workspace_root: Option<&Path>,
1065 ) -> anyhow::Result<Option<Hover>> {
1066 Ok(match identifier_definition {
1067 IdentifierDefinition::Location {
1068 destination,
1069 name,
1070 source,
1071 } => {
1072 find_symbols_at_location(
1076 document.ast.codemap(),
1077 document.ast.statement(),
1078 ResolvedPos {
1079 line: destination.begin.line,
1080 column: destination.begin.column,
1081 },
1082 )
1083 .remove(&name)
1084 .and_then(|symbol| {
1085 symbol
1086 .doc
1087 .map(|docs| Hover {
1088 contents: HoverContents::Array(vec![MarkedString::String(
1089 render_doc_item_no_link(&symbol.name, &docs),
1090 )]),
1091 range: Some(source.into()),
1092 })
1093 .or_else(|| {
1094 symbol.param.map(|(starred_name, doc)| Hover {
1095 contents: HoverContents::Array(vec![MarkedString::String(
1096 render_doc_param(starred_name, &doc),
1097 )]),
1098 range: Some(source.into()),
1099 })
1100 })
1101 })
1102 }
1103 IdentifierDefinition::LoadedLocation {
1104 path, name, source, ..
1105 } => {
1106 let load_uri = self.resolve_load_path(&path, document_uri, workspace_root)?;
1109 self.get_ast_or_load_from_disk(&load_uri)?.and_then(|ast| {
1110 ast.find_exported_symbol(&name).and_then(|symbol| {
1111 symbol.docs.map(|docs| Hover {
1112 contents: HoverContents::Array(vec![MarkedString::String(
1113 render_doc_item_no_link(&symbol.name, &docs),
1114 )]),
1115 range: Some(source.into()),
1116 })
1117 })
1118 })
1119 }
1120 IdentifierDefinition::StringLiteral { source, literal } => {
1121 let Ok(resolved_literal) =
1122 self.context
1123 .resolve_string_literal(&literal, document_uri, workspace_root)
1124 else {
1125 return Ok(None);
1128 };
1129 match resolved_literal {
1130 Some(StringLiteralResult {
1131 url,
1132 location_finder: Some(location_finder),
1133 }) => {
1134 let module = if let Ok(Some(ast)) = self.get_ast_or_load_from_disk(&url) {
1137 ast
1138 } else {
1139 return Ok(None);
1140 };
1141 let result = location_finder(&module.ast)?;
1142
1143 result.map(|location| Hover {
1144 contents: HoverContents::Array(vec![MarkedString::LanguageString(
1145 LanguageString {
1146 language: "python".to_owned(),
1147 value: module.ast.codemap().source_span(location).to_owned(),
1148 },
1149 )]),
1150 range: Some(source.into()),
1151 })
1152 }
1153 _ => None,
1154 }
1155 }
1156 IdentifierDefinition::Unresolved { source, name } => {
1157 self.context
1159 .get_environment(document_uri)
1160 .members
1161 .into_iter()
1162 .find(|symbol| symbol.0 == name)
1163 .map(|symbol| Hover {
1164 contents: HoverContents::Array(vec![MarkedString::String(
1165 render_doc_item_no_link(&symbol.0, &symbol.1),
1166 )]),
1167 range: Some(source.into()),
1168 })
1169 }
1170 IdentifierDefinition::LoadPath { .. } | IdentifierDefinition::NotFound => None,
1171 })
1172 }
1173
1174 fn get_workspace_root(
1175 workspace_roots: Option<&Vec<WorkspaceFolder>>,
1176 target: &LspUrl,
1177 ) -> Option<PathBuf> {
1178 match target {
1179 LspUrl::File(target) => workspace_roots.and_then(|roots| {
1180 roots
1181 .iter()
1182 .filter_map(|root| root.uri.to_file_path().ok())
1183 .find(|root| target.starts_with(root))
1184 }),
1185 _ => None,
1186 }
1187 }
1188}
1189
1190impl<T: LspContext> Backend<T> {
1192 fn send_notification(&self, x: Notification) {
1193 self.connection
1194 .sender
1195 .send(Message::Notification(x))
1196 .unwrap()
1197 }
1198
1199 fn send_response(&self, x: Response) {
1200 self.connection.sender.send(Message::Response(x)).unwrap()
1201 }
1202
1203 fn log_message(&self, typ: MessageType, message: &str) {
1204 self.send_notification(new_notification::<LogMessage>(LogMessageParams {
1205 typ,
1206 message: message.to_owned(),
1207 }))
1208 }
1209
1210 fn publish_diagnostics(&self, uri: Url, diags: Vec<Diagnostic>, version: Option<i64>) {
1211 self.send_notification(new_notification::<PublishDiagnostics>(
1212 PublishDiagnosticsParams::new(uri, diags, version.map(|i| i as i32)),
1213 ));
1214 }
1215
1216 fn main_loop(&self, initialize_params: InitializeParams) -> anyhow::Result<()> {
1217 self.log_message(MessageType::INFO, "Starlark server initialised");
1218 for msg in &self.connection.receiver {
1219 match msg {
1220 Message::Request(req) => {
1221 if let Some(params) = as_request::<GotoDefinition>(&req) {
1224 self.goto_definition(req.id, params, &initialize_params);
1225 } else if let Some(params) = as_request::<StarlarkFileContentsRequest>(&req) {
1226 self.get_starlark_file_contents(req.id, params);
1227 } else if let Some(params) = as_request::<Completion>(&req) {
1228 self.completion(req.id, params, &initialize_params);
1229 } else if let Some(params) = as_request::<HoverRequest>(&req) {
1230 self.hover(req.id, params, &initialize_params);
1231 } else if self.connection.handle_shutdown(&req)? {
1232 return Ok(());
1233 }
1234 }
1236 Message::Notification(x) => {
1237 if let Some(params) = as_notification::<DidOpenTextDocument>(&x) {
1238 self.did_open(params)?;
1239 } else if let Some(params) = as_notification::<DidChangeTextDocument>(&x) {
1240 self.did_change(params)?;
1241 } else if let Some(params) = as_notification::<DidCloseTextDocument>(&x) {
1242 self.did_close(params)?;
1243 }
1244 }
1245 Message::Response(_) => {
1246 }
1248 }
1249 }
1250 Ok(())
1251 }
1252}
1253
1254pub fn stdio_server<T: LspContext>(context: T) -> anyhow::Result<()> {
1256 eprintln!("Starting Rust Starlark server");
1258
1259 let (connection, io_threads) = Connection::stdio();
1260 server_with_connection(connection, context)?;
1261 io_threads.join()?;
1263
1264 eprintln!("Stopping Rust Starlark server");
1265 Ok(())
1266}
1267
1268pub fn server_with_connection<T: LspContext>(
1270 connection: Connection,
1271 context: T,
1272) -> anyhow::Result<()> {
1273 let (init_request_id, init_value) = connection.initialize_start()?;
1275
1276 let initialization_params: InitializeParams = serde_json::from_value(init_value)?;
1277 let server_settings = initialization_params
1278 .initialization_options
1279 .as_ref()
1280 .and_then(|opts| serde_json::from_value(opts.clone()).ok())
1281 .unwrap_or_default();
1282 let capabilities_payload = Backend::<T>::server_capabilities(server_settings);
1283 let server_capabilities = serde_json::to_value(capabilities_payload).unwrap();
1284
1285 let initialize_data = serde_json::json!({
1286 "capabilities": server_capabilities,
1287 });
1288 connection.initialize_finish(init_request_id, initialize_data)?;
1289
1290 Backend {
1291 connection,
1292 context,
1293 last_valid_parse: RwLock::default(),
1294 }
1295 .main_loop(initialization_params)?;
1296
1297 Ok(())
1298}
1299
1300fn as_notification<T>(x: &Notification) -> Option<T::Params>
1301where
1302 T: lsp_types::notification::Notification,
1303 T::Params: DeserializeOwned,
1304{
1305 if x.method == T::METHOD {
1306 let params = serde_json::from_value(x.params.clone()).unwrap_or_else(|err| {
1307 panic!(
1308 "Invalid notification\nMethod: {}\n error: {}",
1309 x.method, err
1310 )
1311 });
1312 Some(params)
1313 } else {
1314 None
1315 }
1316}
1317
1318fn as_request<T>(x: &Request) -> Option<T::Params>
1319where
1320 T: lsp_types::request::Request,
1321 T::Params: DeserializeOwned,
1322{
1323 if x.method == T::METHOD {
1324 let params = serde_json::from_value(x.params.clone()).unwrap_or_else(|err| {
1325 panic!(
1326 "Invalid request\n method: {}\n error: {}\n request: {:?}\n",
1327 x.method, err, x
1328 )
1329 });
1330 Some(params)
1331 } else {
1332 None
1333 }
1334}
1335
1336pub(crate) fn new_notification<T>(params: T::Params) -> Notification
1338where
1339 T: lsp_types::notification::Notification,
1340{
1341 Notification {
1342 method: T::METHOD.to_owned(),
1343 params: serde_json::to_value(¶ms).unwrap(),
1344 }
1345}
1346
1347fn new_response<T>(id: RequestId, params: anyhow::Result<T>) -> Response
1348where
1349 T: serde::Serialize,
1350{
1351 match params {
1352 Ok(params) => Response {
1353 id,
1354 result: Some(serde_json::to_value(params).unwrap()),
1355 error: None,
1356 },
1357 Err(e) => Response {
1358 id,
1359 result: None,
1360 error: Some(ResponseError {
1361 code: 0,
1362 message: format!("{:#?}", e),
1363 data: None,
1364 }),
1365 },
1366 }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371 use std::path::Path;
1372 use std::path::PathBuf;
1373
1374 use anyhow::Context;
1375 use lsp_server::Request;
1376 use lsp_server::RequestId;
1377 use lsp_types::request::GotoDefinition;
1378 use lsp_types::GotoDefinitionParams;
1379 use lsp_types::GotoDefinitionResponse;
1380 use lsp_types::LocationLink;
1381 use lsp_types::Position;
1382 use lsp_types::Range;
1383 use lsp_types::TextDocumentIdentifier;
1384 use lsp_types::TextDocumentPositionParams;
1385 use lsp_types::Url;
1386 use starlark::codemap::ResolvedSpan;
1387 use starlark::wasm::is_wasm;
1388 use textwrap::dedent;
1389
1390 use crate::definition::helpers::FixtureWithRanges;
1391 use crate::server::LspServerSettings;
1392 use crate::server::LspUrl;
1393 use crate::server::StarlarkFileContentsParams;
1394 use crate::server::StarlarkFileContentsRequest;
1395 use crate::server::StarlarkFileContentsResponse;
1396 use crate::test::TestServer;
1397
1398 fn goto_definition_request(
1399 server: &mut TestServer,
1400 uri: Url,
1401 line: u32,
1402 character: u32,
1403 ) -> Request {
1404 server.new_request::<GotoDefinition>(GotoDefinitionParams {
1405 text_document_position_params: TextDocumentPositionParams {
1406 text_document: TextDocumentIdentifier { uri },
1407 position: Position { line, character },
1408 },
1409 work_done_progress_params: Default::default(),
1410 partial_result_params: Default::default(),
1411 })
1412 }
1413
1414 fn goto_definition_response_location(
1415 server: &mut TestServer,
1416 request_id: RequestId,
1417 ) -> anyhow::Result<LocationLink> {
1418 let response = server.get_response::<GotoDefinitionResponse>(request_id)?;
1419 match response {
1420 GotoDefinitionResponse::Link(locations) if locations.len() == 1 => {
1421 Ok(locations[0].clone())
1422 }
1423 _ => Err(anyhow::anyhow!("Got invalid message type: {:?}", response)),
1424 }
1425 }
1426
1427 fn expected_location_link(
1428 uri: Url,
1429 source_line: u32,
1430 source_start_col: u32,
1431 source_end_col: u32,
1432 dest_line: u32,
1433 dest_start_col: u32,
1434 dest_end_col: u32,
1435 ) -> LocationLink {
1436 let source_range = Range::new(
1437 Position::new(source_line, source_start_col),
1438 Position::new(source_line, source_end_col),
1439 );
1440 let dest_range = Range::new(
1441 Position::new(dest_line, dest_start_col),
1442 Position::new(dest_line, dest_end_col),
1443 );
1444 LocationLink {
1445 origin_selection_range: Some(source_range),
1446 target_uri: uri,
1447 target_range: dest_range,
1448 target_selection_range: dest_range,
1449 }
1450 }
1451
1452 fn expected_location_link_from_spans(
1453 uri: Url,
1454 source_span: ResolvedSpan,
1455 dest_span: ResolvedSpan,
1456 ) -> LocationLink {
1457 LocationLink {
1458 origin_selection_range: Some(source_span.into()),
1459 target_uri: uri,
1460 target_range: dest_span.into(),
1461 target_selection_range: dest_span.into(),
1462 }
1463 }
1464
1465 #[cfg(windows)]
1466 fn temp_file_uri(rel_path: &str) -> Url {
1467 Url::from_file_path(&PathBuf::from("C:/tmp").join(rel_path)).unwrap()
1468 }
1469
1470 #[cfg(not(windows))]
1471 fn temp_file_uri(rel_path: &str) -> Url {
1472 Url::from_file_path(PathBuf::from("/tmp").join(rel_path)).unwrap()
1473 }
1474
1475 fn path_to_load_string(p: &Path) -> String {
1478 p.to_str().unwrap().replace('\\', "/")
1479 }
1480
1481 fn uri_to_load_string(uri: &Url) -> String {
1482 path_to_load_string(&uri.to_file_path().unwrap())
1483 }
1484
1485 #[test]
1486 fn sends_empty_goto_definition_on_nonexistent_file() -> anyhow::Result<()> {
1487 if is_wasm() {
1488 return Ok(());
1489 }
1490
1491 let mut server = TestServer::new()?;
1492 let req = goto_definition_request(&mut server, temp_file_uri("nonexistent"), 0, 0);
1493
1494 let request_id = server.send_request(req)?;
1495 let response: GotoDefinitionResponse = server.get_response(request_id)?;
1496 match response {
1497 GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
1498 response => Err(anyhow::anyhow!(
1499 "Expected empty definitions, got `{:?}`",
1500 response
1501 )),
1502 }
1503 }
1504
1505 #[test]
1506 fn sends_empty_goto_definition_on_non_access_symbol() -> anyhow::Result<()> {
1507 if is_wasm() {
1508 return Ok(());
1509 }
1510
1511 let uri = temp_file_uri("file.star");
1512
1513 let mut server = TestServer::new()?;
1514 let contents = "y = 1\ndef nothing():\n pass\nprint(nothing())\n";
1515 server.open_file(uri.clone(), contents.to_owned())?;
1516
1517 let goto_definition = goto_definition_request(&mut server, uri, 1, 6);
1518
1519 let request_id = server.send_request(goto_definition)?;
1520 let response = server.get_response::<GotoDefinitionResponse>(request_id)?;
1521 match response {
1522 GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
1523 response => Err(anyhow::anyhow!(
1524 "Expected empty definitions, got `{:?}`",
1525 response
1526 )),
1527 }
1528 }
1529
1530 #[test]
1531 fn goes_to_definition() -> anyhow::Result<()> {
1532 if is_wasm() {
1533 return Ok(());
1534 }
1535
1536 let uri = temp_file_uri("file.star");
1537 let expected_location = expected_location_link(uri.clone(), 3, 6, 13, 1, 4, 11);
1538
1539 let mut server = TestServer::new()?;
1540 let contents = "y = 1\ndef nothing():\n pass\nprint(nothing())\n";
1541 server.open_file(uri.clone(), contents.to_owned())?;
1542
1543 let goto_definition = goto_definition_request(&mut server, uri, 3, 6);
1544
1545 let request_id = server.send_request(goto_definition)?;
1546 let location = goto_definition_response_location(&mut server, request_id)?;
1547
1548 assert_eq!(expected_location, location);
1549 Ok(())
1550 }
1551
1552 #[test]
1553 fn returns_old_definitions_if_current_file_does_not_parse() -> anyhow::Result<()> {
1554 if is_wasm() {
1555 return Ok(());
1556 }
1557
1558 let uri = temp_file_uri("file.star");
1559 let expected_location = expected_location_link(uri.clone(), 3, 6, 13, 1, 4, 11);
1560
1561 let mut server = TestServer::new()?;
1562 let contents = "y = 1\ndef nothing():\n pass\nprint(nothing())\n";
1563 server.open_file(uri.clone(), contents.to_owned())?;
1564 server.change_file(uri.clone(), "\"invalid parse".to_owned())?;
1565
1566 let goto_definition = goto_definition_request(&mut server, uri, 3, 6);
1567
1568 let request_id = server.send_request(goto_definition)?;
1569 let location = goto_definition_response_location(&mut server, request_id)?;
1570
1571 assert_eq!(expected_location, location);
1572 Ok(())
1573 }
1574
1575 #[test]
1576 fn jumps_to_definition_from_opened_loaded_file() -> anyhow::Result<()> {
1577 if is_wasm() {
1578 return Ok(());
1579 }
1580
1581 let foo_uri = temp_file_uri("foo.star");
1582 let bar_uri = temp_file_uri("bar.star");
1583
1584 let foo_contents = dedent(
1585 r#"
1586 load("{load}", "baz")
1587 <baz_click><baz>b</baz>az</baz_click>()
1588 "#,
1589 )
1590 .replace("{load}", &uri_to_load_string(&bar_uri))
1591 .trim()
1592 .to_owned();
1593 let bar_contents = "def <baz>baz</baz>():\n pass";
1594 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1595 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1596
1597 let expected_location = expected_location_link_from_spans(
1598 bar_uri.clone(),
1599 foo.resolved_span("baz_click"),
1600 bar.resolved_span("baz"),
1601 );
1602
1603 let mut server = TestServer::new()?;
1604 server.set_file_contents(&bar_uri, "some_symbol = 1".to_owned())?;
1607 server.open_file(foo_uri.clone(), foo.program())?;
1608 server.open_file(bar_uri, bar.program())?;
1609
1610 let goto_definition = goto_definition_request(
1611 &mut server,
1612 foo_uri,
1613 foo.begin_line("baz"),
1614 foo.begin_column("baz"),
1615 );
1616
1617 let request_id = server.send_request(goto_definition)?;
1618 let location = goto_definition_response_location(&mut server, request_id)?;
1619
1620 assert_eq!(expected_location, location);
1621 Ok(())
1622 }
1623
1624 #[test]
1625 fn jumps_to_definition_from_closed_loaded_file() -> anyhow::Result<()> {
1626 if is_wasm() {
1627 return Ok(());
1628 }
1629
1630 let foo_uri = temp_file_uri("foo.star");
1631 let bar_uri = temp_file_uri("bar.star");
1632
1633 let foo_contents = dedent(
1634 r#"
1635 load("{load}", "baz")
1636 <baz_click><baz>b</baz>az</baz_click>()
1637 "#,
1638 )
1639 .replace("{load}", &uri_to_load_string(&bar_uri))
1640 .trim()
1641 .to_owned();
1642 eprintln!("foo_contents: {}", foo_contents);
1643 let bar_contents = "def <baz>baz</baz>():\n pass";
1644 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1645 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1646
1647 let expected_location = expected_location_link_from_spans(
1648 bar_uri.clone(),
1649 foo.resolved_span("baz_click"),
1650 bar.resolved_span("baz"),
1651 );
1652
1653 let mut server = TestServer::new()?;
1654 server.open_file(foo_uri.clone(), foo.program())?;
1655 server.set_file_contents(&bar_uri, bar.program())?;
1656
1657 let goto_definition = goto_definition_request(
1658 &mut server,
1659 foo_uri,
1660 foo.begin_line("baz"),
1661 foo.begin_column("baz"),
1662 );
1663
1664 let request_id = server.send_request(goto_definition)?;
1665 let location = goto_definition_response_location(&mut server, request_id)?;
1666
1667 assert_eq!(expected_location, location);
1668 Ok(())
1669 }
1670
1671 #[test]
1672 fn passes_cwd_for_relative_loads() -> anyhow::Result<()> {
1673 if is_wasm() {
1674 return Ok(());
1675 }
1676
1677 let foo_uri = temp_file_uri("foo.star");
1678 let bar_uri = temp_file_uri("bar.star");
1679
1680 let foo_contents = dedent(
1681 r#"
1682 load("bar.star", "baz")
1683 <baz_click><baz>b</baz>az</baz_click>()
1684 "#,
1685 )
1686 .trim()
1687 .to_owned();
1688 let bar_contents = "def <baz>baz</baz>():\n pass";
1689 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1690 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1691
1692 let expected_location = expected_location_link_from_spans(
1693 bar_uri.clone(),
1694 foo.resolved_span("baz_click"),
1695 bar.resolved_span("baz"),
1696 );
1697
1698 let mut server = TestServer::new()?;
1699 server.open_file(foo_uri.clone(), foo.program())?;
1700 server.set_file_contents(&bar_uri, bar.program())?;
1701
1702 let goto_definition = goto_definition_request(
1703 &mut server,
1704 foo_uri,
1705 foo.begin_line("baz"),
1706 foo.begin_column("baz"),
1707 );
1708
1709 let request_id = server.send_request(goto_definition)?;
1710 let location = goto_definition_response_location(&mut server, request_id)?;
1711
1712 assert_eq!(expected_location, location);
1713 Ok(())
1714 }
1715
1716 #[test]
1717 fn does_not_jump_to_definition_if_invalid_file() -> anyhow::Result<()> {
1718 if is_wasm() {
1719 return Ok(());
1720 }
1721
1722 let foo_uri = temp_file_uri("foo.star");
1723
1724 let foo_contents = dedent(
1725 r#"
1726 load("{load}", <baz_loc>"baz"</baz_loc>)
1727 <baz_click><baz>b</baz>az</baz_click>()
1728 "#,
1729 )
1730 .replace("{load}", foo_uri.path())
1731 .trim()
1732 .to_owned();
1733 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1734 let expected_location = expected_location_link_from_spans(
1735 foo_uri.clone(),
1736 foo.resolved_span("baz_click"),
1737 foo.resolved_span("baz_loc"),
1738 );
1739
1740 let mut server = TestServer::new()?;
1741 server.open_file(foo_uri.clone(), foo.program())?;
1742
1743 let goto_definition = goto_definition_request(
1744 &mut server,
1745 foo_uri,
1746 foo.begin_line("baz"),
1747 foo.begin_column("baz"),
1748 );
1749
1750 let request_id = server.send_request(goto_definition)?;
1751 let location = goto_definition_response_location(&mut server, request_id)?;
1752
1753 assert_eq!(expected_location, location);
1754 Ok(())
1755 }
1756
1757 #[test]
1758 fn does_not_jump_to_definition_if_symbol_not_found() -> anyhow::Result<()> {
1759 if is_wasm() {
1760 return Ok(());
1761 }
1762
1763 let foo_uri = temp_file_uri("foo.star");
1764 let bar_uri = temp_file_uri("bar.star");
1765
1766 let foo_contents = dedent(
1767 r#"
1768 load("bar.star", <not_baz_loc>"not_baz"</not_baz_loc>)
1769 <not_baz>not_baz</not_baz>()
1770 "#,
1771 )
1772 .trim()
1773 .to_owned();
1774 let bar_contents = "def baz():\n pass";
1775 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1776 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1777
1778 let expected_location = expected_location_link_from_spans(
1779 foo_uri.clone(),
1780 foo.resolved_span("not_baz"),
1781 foo.resolved_span("not_baz_loc"),
1782 );
1783
1784 let mut server = TestServer::new()?;
1785 server.open_file(foo_uri.clone(), foo.program())?;
1786 server.set_file_contents(&bar_uri, bar.program())?;
1787
1788 let goto_definition = goto_definition_request(
1789 &mut server,
1790 foo_uri,
1791 foo.begin_line("not_baz"),
1792 foo.begin_column("not_baz"),
1793 );
1794
1795 let request_id = server.send_request(goto_definition)?;
1796 let location = goto_definition_response_location(&mut server, request_id)?;
1797
1798 assert_eq!(expected_location, location);
1799 Ok(())
1800 }
1801
1802 #[test]
1803 fn jumps_to_definition_in_load_statement() -> anyhow::Result<()> {
1804 if is_wasm() {
1805 return Ok(());
1806 }
1807
1808 let foo_uri = temp_file_uri("foo.star");
1809 let bar_uri = temp_file_uri("bar.star");
1810
1811 let foo_contents = dedent(
1812 r#"
1813 load("{load}", <baz_click>"<baz>b</baz>az"</baz_click>)
1814 baz()
1815 "#,
1816 )
1817 .replace("{load}", &uri_to_load_string(&bar_uri))
1818 .trim()
1819 .to_owned();
1820 let bar_contents = "def <baz>baz</baz>():\n pass";
1821 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1822 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1823
1824 let expected_location = expected_location_link_from_spans(
1825 bar_uri.clone(),
1826 foo.resolved_span("baz_click"),
1827 bar.resolved_span("baz"),
1828 );
1829
1830 let mut server = TestServer::new()?;
1831 server.open_file(foo_uri.clone(), foo.program())?;
1832 server.set_file_contents(&bar_uri, bar.program())?;
1833
1834 let goto_definition = goto_definition_request(
1835 &mut server,
1836 foo_uri,
1837 foo.begin_line("baz"),
1838 foo.begin_column("baz"),
1839 );
1840
1841 let request_id = server.send_request(goto_definition)?;
1842 let location = goto_definition_response_location(&mut server, request_id)?;
1843
1844 assert_eq!(expected_location, location);
1845 Ok(())
1846 }
1847
1848 #[test]
1849 fn does_not_jump_to_definition_in_load_statement_if_not_found() -> anyhow::Result<()> {
1850 if is_wasm() {
1851 return Ok(());
1852 }
1853
1854 let foo_uri = temp_file_uri("foo.star");
1855 let bar_uri = temp_file_uri("bar.star");
1856
1857 let foo_contents = dedent(
1858 r#"
1859 load("bar.star", <not_baz_loc>"no<not_baz>t</not_baz>_baz"</not_baz_loc>)
1860 not_baz()
1861 "#,
1862 )
1863 .trim()
1864 .to_owned();
1865 let bar_contents = "def baz():\n pass";
1866 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1867 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1868
1869 let expected_location = expected_location_link_from_spans(
1870 foo_uri.clone(),
1871 foo.resolved_span("not_baz_loc"),
1872 foo.resolved_span("not_baz_loc"),
1873 );
1874
1875 let mut server = TestServer::new()?;
1876 server.open_file(foo_uri.clone(), foo.program())?;
1877 server.set_file_contents(&bar_uri, bar.program())?;
1878
1879 let goto_definition = goto_definition_request(
1880 &mut server,
1881 foo_uri,
1882 foo.begin_line("not_baz"),
1883 foo.begin_column("not_baz"),
1884 );
1885
1886 let request_id = server.send_request(goto_definition)?;
1887 let location = goto_definition_response_location(&mut server, request_id)?;
1888
1889 assert_eq!(expected_location, location);
1890 Ok(())
1891 }
1892
1893 #[test]
1894 fn jumps_to_file_in_load_statement() -> anyhow::Result<()> {
1895 if is_wasm() {
1896 return Ok(());
1897 }
1898
1899 let foo_uri = temp_file_uri("foo.star");
1900 let bar_uri = temp_file_uri("bar.star");
1901 let load_path = bar_uri
1902 .to_file_path()
1903 .unwrap()
1904 .parent()
1905 .unwrap()
1906 .join("<bar>b</bar>ar<dot>.</dot>star");
1907 let bar_load_string = path_to_load_string(&load_path);
1908
1909 let foo_contents = dedent(
1910 r#"
1911 load(<bar_click>"{load}"</bar_click>, "baz")
1912 baz()
1913 "#,
1914 )
1915 .replace("{load}", &bar_load_string)
1916 .trim()
1917 .to_owned();
1918 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
1919
1920 let expected_location = LocationLink {
1921 origin_selection_range: Some(foo.resolved_span("bar_click").into()),
1922 target_uri: bar_uri,
1923 target_range: Default::default(),
1924 target_selection_range: Default::default(),
1925 };
1926
1927 let mut server = TestServer::new()?;
1928 server.open_file(foo_uri.clone(), foo.program())?;
1929
1930 let goto_definition = goto_definition_request(
1931 &mut server,
1932 foo_uri.clone(),
1933 foo.begin_line("bar"),
1934 foo.begin_column("bar"),
1935 );
1936
1937 let request_id = server.send_request(goto_definition)?;
1938 let location = goto_definition_response_location(&mut server, request_id)?;
1939
1940 assert_eq!(expected_location, location);
1941
1942 let goto_definition = goto_definition_request(
1943 &mut server,
1944 foo_uri,
1945 foo.begin_line("dot"),
1946 foo.begin_column("dot"),
1947 );
1948
1949 let request_id = server.send_request(goto_definition)?;
1950 let location = goto_definition_response_location(&mut server, request_id)?;
1951
1952 assert_eq!(expected_location, location);
1953
1954 Ok(())
1955 }
1956
1957 #[test]
1958 fn jumps_to_definitions_in_strings() -> anyhow::Result<()> {
1959 if is_wasm() {
1960 return Ok(());
1961 }
1962
1963 let foo_uri = temp_file_uri("foo.star");
1964 let bar_uri = temp_file_uri("bar.star");
1965
1966 let bar_contents = r#""Just <bar>a string</bar>""#;
1967 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
1968 let bar_resolved_span = bar.resolved_span("bar");
1969 let bar_span_str = format!(
1970 "{}:{}",
1971 bar_resolved_span.begin.column, bar_resolved_span.end.column
1972 );
1973
1974 let foo_contents = dedent(
1975 r#"
1976 <foo1_click>"ba<foo1>r</foo1>.star"</foo1_click>
1977 [
1978 <foo2_click>"ba<foo2>r</foo2>.star"</foo2_click>,
1979 "ignored"
1980 ]
1981 {
1982 <foo3_click>"ba<foo3>r</foo3>.star"</foo3_click>: "ignored",
1983 "ignored": <foo4_click>"ba<foo4>r</foo4>.star"</foo4_click>,
1984 "ignored_other": "ignored",
1985 }
1986
1987 def f1(x = <foo5_click>"ba<foo5>r</foo5>.star"</foo5_click>):
1988 <foo6_click>"ba<foo6>r</foo6>.star"</foo6_click>
1989 [
1990 <foo7_click>"ba<foo7>r</foo7>.star"</foo7_click>,
1991 "ignored"
1992 ]
1993 {
1994 <foo8_click>"ba<foo8>r</foo8>.star"</foo8_click>: "ignored",
1995 "ignored": <foo9_click>"ba<foo9>r</foo9>.star"</foo9_click>,
1996 "ignored_other": "ignored",
1997 }
1998 if x == <foo10_click>"ba<foo10>r</foo10>.star"</foo10_click>:
1999 <foo11_click>"ba<foo11>r</foo11>.star"</foo11_click>
2000 [
2001 <foo12_click>"ba<foo12>r</foo12>.star"</foo12_click>,
2002 "ignored"
2003 ]
2004 {
2005 <foo13_click>"ba<foo13>r</foo13>.star"</foo13_click>: "ignored",
2006 "ignored": <foo14_click>"ba<foo14>r</foo14>.star"</foo14_click>,
2007 "ignored_other": "ignored",
2008 }
2009 return <foo15_click>"ba<foo15>r</foo15>.star"</foo15_click>
2010
2011 foo16 = <foo16_click>"ba<foo16>r</foo16>.star"</foo16_click>
2012
2013 <baz1_click>"ba<baz1>r</baz1>.star--{bar_range}"</baz1_click>
2014 [
2015 <baz2_click>"ba<baz2>r</baz2>.star--{bar_range}"</baz2_click>,
2016 "ignored"
2017 ]
2018 {
2019 <baz3_click>"ba<baz3>r</baz3>.star--{bar_range}"</baz3_click>: "ignored",
2020 "ignored": <baz4_click>"ba<baz4>r</baz4>.star--{bar_range}"</baz4_click>,
2021 "ignored_other": "ignored",
2022 }
2023
2024 def f2(x = <baz5_click>"ba<baz5>r</baz5>.star--{bar_range}"</baz5_click>):
2025 <baz6_click>"ba<baz6>r</baz6>.star--{bar_range}"</baz6_click>
2026 [
2027 <baz7_click>"ba<baz7>r</baz7>.star--{bar_range}"</baz7_click>,
2028 "ignored"
2029 ]
2030 {
2031 <baz8_click>"ba<baz8>r</baz8>.star--{bar_range}"</baz8_click>: "ignored",
2032 "ignored": <baz9_click>"ba<baz9>r</baz9>.star--{bar_range}"</baz9_click>,
2033 "ignored_other": "ignored",
2034 }
2035 if x == <baz10_click>"ba<baz10>r</baz10>.star--{bar_range}"</baz10_click>:
2036 <baz11_click>"ba<baz11>r</baz11>.star--{bar_range}"</baz11_click>
2037 [
2038 <baz12_click>"ba<baz12>r</baz12>.star--{bar_range}"</baz12_click>,
2039 "ignored"
2040 ]
2041 {
2042 <baz13_click>"ba<baz13>r</baz13>.star--{bar_range}"</baz13_click>: "ignored",
2043 "ignored": <baz14_click>"ba<baz14>r</baz14>.star--{bar_range}"</baz14_click>,
2044 "ignored_other": "ignored",
2045 }
2046 return <baz15_click>"ba<baz15>r</baz15>.star--{bar_range}"</baz15_click>
2047
2048 baz16 = <baz16_click>"ba<baz16>r</baz16>.star--{bar_range}"</baz16_click>
2049 "#,
2050 )
2051 .trim()
2052 .replace("{bar_range}", &bar_span_str);
2053 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
2054
2055 let mut server = TestServer::new()?;
2056 server.open_file(foo_uri.clone(), foo.program())?;
2057 server.open_file(bar_uri.clone(), bar.program())?;
2058
2059 let mut test = |name: &str, expect_range: bool| -> anyhow::Result<()> {
2060 let range = if expect_range {
2061 bar_resolved_span
2062 } else {
2063 Default::default()
2064 };
2065 let expected_location = expected_location_link_from_spans(
2066 bar_uri.clone(),
2067 foo.resolved_span(&format!("{}_click", name)),
2068 range,
2069 );
2070
2071 let goto_definition = goto_definition_request(
2072 &mut server,
2073 foo_uri.clone(),
2074 foo.begin_line(name),
2075 foo.begin_column(name),
2076 );
2077 let request_id = server.send_request(goto_definition)?;
2078 let location = goto_definition_response_location(&mut server, request_id)?;
2079
2080 assert_eq!(expected_location, location);
2081 Ok(())
2082 };
2083
2084 test("foo1", false)?;
2085 test("foo2", false)?;
2086 test("foo3", false)?;
2087 test("foo4", false)?;
2088 test("foo5", false)?;
2089 test("foo6", false)?;
2090 test("foo7", false)?;
2091 test("foo8", false)?;
2092 test("foo9", false)?;
2093 test("foo10", false)?;
2094 test("foo11", false)?;
2095 test("foo12", false)?;
2096 test("foo13", false)?;
2097 test("foo14", false)?;
2098 test("foo15", false)?;
2099 test("foo16", false)?;
2100
2101 test("baz1", true)?;
2102 test("baz2", true)?;
2103 test("baz3", true)?;
2104 test("baz4", true)?;
2105 test("baz5", true)?;
2106 test("baz6", true)?;
2107 test("baz7", true)?;
2108 test("baz8", true)?;
2109 test("baz9", true)?;
2110 test("baz10", true)?;
2111 test("baz11", true)?;
2112 test("baz12", true)?;
2113 test("baz13", true)?;
2114 test("baz14", true)?;
2115 test("baz15", true)?;
2116 test("baz16", true)?;
2117
2118 Ok(())
2119 }
2120
2121 #[test]
2122 fn handles_paths_that_are_not_starlark() -> anyhow::Result<()> {
2123 if is_wasm() {
2124 return Ok(());
2125 }
2126
2127 let foo_uri = temp_file_uri("foo.star");
2128 let bar_uri = temp_file_uri("bar.star");
2129 let baz_uri = temp_file_uri("baz");
2130 let dir1_uri = temp_file_uri("dir1");
2131 let dir2_uri = temp_file_uri("dir2.star");
2132
2133 let foo_contents = dedent(
2134 r#"
2135 <bar>"b<bar_click>a</bar_click>r.star"</bar>
2136 <baz>"b<baz_click>a</baz_click>z"</baz>
2137 <dir1>"d<dir1_click>i</dir1_click>r1"</dir1>
2138 <dir2>"d<dir2_click>i</dir2_click>r2.star"</dir2>
2139 "#,
2140 )
2141 .trim()
2142 .to_owned();
2143
2144 let bar_contents = dedent(
2145 r#"
2146 def ba r():
2147 # This has broken syntax
2148 pass
2149 "#,
2150 )
2151 .trim()
2152 .to_owned();
2153
2154 let foo = FixtureWithRanges::from_fixture("foo.star", &foo_contents)?;
2155 let bar = FixtureWithRanges::from_fixture("bar.star", &bar_contents)?;
2156
2157 let mut server = TestServer::new()?;
2158 server.open_file(foo_uri.clone(), foo.program())?;
2159 server.set_file_contents(&bar_uri, bar.program())?;
2160 server.mkdir(dir1_uri.clone());
2161 server.mkdir(dir2_uri.clone());
2162
2163 let goto_definition = goto_definition_request(
2165 &mut server,
2166 foo_uri.clone(),
2167 foo.begin_line("bar_click"),
2168 foo.begin_column("bar_click"),
2169 );
2170 let req_id = server.send_request(goto_definition)?;
2171 let response = goto_definition_response_location(&mut server, req_id)?;
2172
2173 let expected = LocationLink {
2174 origin_selection_range: Some(foo.resolved_span("bar").into()),
2175 target_uri: bar_uri,
2176 target_range: Default::default(),
2177 target_selection_range: Default::default(),
2178 };
2179 assert_eq!(expected, response);
2180
2181 let goto_definition = goto_definition_request(
2183 &mut server,
2184 foo_uri.clone(),
2185 foo.begin_line("baz_click"),
2186 foo.begin_column("baz_click"),
2187 );
2188 let req_id = server.send_request(goto_definition)?;
2189 let response = goto_definition_response_location(&mut server, req_id)?;
2190
2191 let expected = LocationLink {
2192 origin_selection_range: Some(foo.resolved_span("baz").into()),
2193 target_uri: baz_uri,
2194 target_range: Default::default(),
2195 target_selection_range: Default::default(),
2196 };
2197 assert_eq!(expected, response);
2198
2199 let goto_definition = goto_definition_request(
2201 &mut server,
2202 foo_uri.clone(),
2203 foo.begin_line("dir1_click"),
2204 foo.begin_column("dir1_click"),
2205 );
2206 let req_id = server.send_request(goto_definition)?;
2207 let response = goto_definition_response_location(&mut server, req_id)?;
2208
2209 let expected = LocationLink {
2210 origin_selection_range: Some(foo.resolved_span("dir1").into()),
2211 target_uri: dir1_uri,
2212 target_range: Default::default(),
2213 target_selection_range: Default::default(),
2214 };
2215 assert_eq!(expected, response);
2216
2217 let goto_definition = goto_definition_request(
2220 &mut server,
2221 foo_uri,
2222 foo.begin_line("dir2_click"),
2223 foo.begin_column("dir2_click"),
2224 );
2225 let req_id = server.send_request(goto_definition)?;
2226 let response = goto_definition_response_location(&mut server, req_id)?;
2227
2228 let expected = LocationLink {
2229 origin_selection_range: Some(foo.resolved_span("dir2").into()),
2230 target_uri: dir2_uri,
2231 target_range: Default::default(),
2232 target_selection_range: Default::default(),
2233 };
2234 assert_eq!(expected, response);
2235 Ok(())
2236 }
2237
2238 #[test]
2239 fn disables_goto_definition() -> anyhow::Result<()> {
2240 if is_wasm() {
2241 return Ok(());
2242 }
2243
2244 let server = TestServer::new_with_settings(Some(LspServerSettings {
2245 enable_goto_definition: false,
2246 }))?;
2247
2248 let goto_definition_disabled = server
2249 .initialization_result()
2250 .unwrap()
2251 .capabilities
2252 .definition_provider
2253 .is_none();
2254
2255 assert!(goto_definition_disabled);
2256
2257 let server = TestServer::new_with_settings(Some(LspServerSettings {
2258 enable_goto_definition: true,
2259 }))?;
2260
2261 let goto_definition_enabled = server
2262 .initialization_result()
2263 .unwrap()
2264 .capabilities
2265 .definition_provider
2266 .is_some();
2267
2268 assert!(goto_definition_enabled);
2269 Ok(())
2270 }
2271
2272 #[test]
2273 fn returns_starlark_file_contents() -> anyhow::Result<()> {
2274 if is_wasm() {
2275 return Ok(());
2276 }
2277
2278 let mut server = TestServer::new()?;
2279
2280 let uri = LspUrl::try_from(Url::parse("starlark:/native/builtin.bzl")?)?;
2281 let req = server.new_request::<StarlarkFileContentsRequest>(StarlarkFileContentsParams {
2282 uri: uri.clone(),
2283 });
2284 let request_id = server.send_request(req)?;
2285 let response = server.get_response::<StarlarkFileContentsResponse>(request_id)?;
2286 assert_eq!(
2287 server.docs_as_code(&uri).unwrap(),
2288 response.contents.unwrap()
2289 );
2290
2291 let req = server.new_request::<StarlarkFileContentsRequest>(StarlarkFileContentsParams {
2292 uri: LspUrl::try_from(Url::parse("starlark:/native/not_builtin.bzl")?)?,
2293 });
2294 let request_id = server.send_request(req)?;
2295 let response = server.get_response::<StarlarkFileContentsResponse>(request_id)?;
2296 assert!(response.contents.is_none());
2297
2298 Ok(())
2299 }
2300
2301 fn resolve_range_in_string(s: &str, r: Range) -> &str {
2302 let byte_of_pos = |p: Position| {
2303 let l = if p.line == 0 {
2304 0
2305 } else {
2306 s.char_indices()
2307 .filter(|(_, c)| *c == '\n')
2308 .nth((p.line - 1).try_into().unwrap())
2309 .unwrap()
2310 .0
2311 + 1
2312 };
2313 l + s[l..]
2314 .char_indices()
2315 .nth((p.character).try_into().unwrap())
2316 .unwrap()
2317 .0
2318 };
2319 let start = byte_of_pos(r.start);
2320 let end = byte_of_pos(r.end);
2321 &s[start..end]
2322 }
2323
2324 #[test]
2325 fn goto_works_for_native_symbols() -> anyhow::Result<()> {
2326 if is_wasm() {
2327 return Ok(());
2328 }
2329
2330 let foo_uri = temp_file_uri("foo.star");
2331 let native_uri = Url::parse("starlark:/native/builtin.bzl")?;
2332
2333 let mut server = TestServer::new()?;
2334
2335 let foo_contents = dedent(
2336 r#"
2337 <click_n1>na<n1>t</n1>ive_function1</click_n1>()
2338 def f(<n2_loc>native_function1</n2_loc>):
2339 print(<click_n2>nat<n2>i</n2>ve_function1</click_n2>)
2340 mi<n3>s</n3>sing_global()
2341 "#,
2342 )
2343 .trim()
2344 .to_owned();
2345
2346 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
2347
2348 server.open_file(foo_uri.clone(), foo.program())?;
2349
2350 let goto_definition = goto_definition_request(
2351 &mut server,
2352 foo_uri.clone(),
2353 foo.begin_line("n1"),
2354 foo.begin_column("n1"),
2355 );
2356 let request_id = server.send_request(goto_definition)?;
2357 let n1_location = goto_definition_response_location(&mut server, request_id)?;
2358
2359 assert_eq!(
2360 n1_location.origin_selection_range,
2361 Some(foo.resolved_span("click_n1").into())
2362 );
2363 assert_eq!(n1_location.target_uri, native_uri);
2364 let native_gen_code = server
2365 .docs_as_code(&native_uri.try_into().unwrap())
2366 .unwrap();
2367 let target_str = resolve_range_in_string(&native_gen_code, n1_location.target_range);
2368 assert_eq!(target_str, "native_function1");
2369
2370 let expected_n2_location = expected_location_link_from_spans(
2371 foo_uri.clone(),
2372 foo.resolved_span("click_n2"),
2373 foo.resolved_span("n2_loc"),
2374 );
2375
2376 let goto_definition = goto_definition_request(
2377 &mut server,
2378 foo_uri.clone(),
2379 foo.begin_line("n2"),
2380 foo.begin_column("n2"),
2381 );
2382 let request_id = server.send_request(goto_definition)?;
2383 let n2_location = goto_definition_response_location(&mut server, request_id)?;
2384
2385 assert_eq!(expected_n2_location, n2_location);
2386
2387 let goto_definition = goto_definition_request(
2388 &mut server,
2389 foo_uri,
2390 foo.begin_line("n3"),
2391 foo.begin_column("n3"),
2392 );
2393 let request_id = server.send_request(goto_definition)?;
2394 let n3_response = server.get_response::<GotoDefinitionResponse>(request_id)?;
2395 match n3_response {
2396 GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
2397 response => Err(anyhow::anyhow!(
2398 "Expected empty definitions, got `{:?}`",
2399 response
2400 )),
2401 }?;
2402
2403 Ok(())
2404 }
2405
2406 #[test]
2407 fn jumps_to_original_member_definition() -> anyhow::Result<()> {
2408 if is_wasm() {
2409 return Ok(());
2410 }
2411
2412 let foo_uri = temp_file_uri("foo.star");
2413 let bar_uri = temp_file_uri("bar.star");
2414
2415 let foo_contents = dedent(
2416 r#"
2417 load("{load}", "loaded")
2418
2419 def <dest_baz>_baz</dest_baz>():
2420 pass
2421
2422 <dest_quz>_quz</dest_quz> = 6
2423
2424 <dest_root><dest_foobar>FooBarModule</dest_foobar></dest_root> = 5
2425 FooModule = struct(<dest_foo>foo</dest_foo> = 5)
2426 # Member value does not exist
2427 BarModule = struct(<dest_bar>bar</dest_bar> = bar)
2428 BazModule = struct(
2429 bar = bar,
2430 baz = _baz,
2431 )
2432 QuzModule = struct(bar = bar, baz = _baz, quz = _quz)
2433
2434 <root>Foo<root_click>Bar</root_click>Module</root>.<foobar>f<foobar_click>o</foobar_click>obar</foobar>
2435 FooModule.<foo>f<foo_click>o</foo_click>o</foo>
2436 BarModule.<bar>b<bar_click>a</bar_click>r</bar>
2437 BazModule.<baz>b<baz_click>a</baz_click>z</baz>
2438 QuzModule.<quz>q<quz_click>u</quz_click>z</quz>
2439 loaded.<x><x_click>x</x_click></x>
2440 loaded.<y><y_click>y</y_click></y>
2441 "#,
2442 )
2443 .replace("{load}", &uri_to_load_string(&bar_uri))
2444 .trim()
2445 .to_owned();
2446
2447 let bar_contents = dedent(
2448 r#"
2449 def <dest_x>_x</dest_x>():
2450 pass
2451 <dest_y>loaded</dest_y> = struct(x = _x)
2452 "#,
2453 )
2454 .trim()
2455 .to_owned();
2456
2457 let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
2458 let bar = FixtureWithRanges::from_fixture(bar_uri.path(), &bar_contents)?;
2459
2460 let mut server = TestServer::new()?;
2461 server.open_file(foo_uri.clone(), foo.program())?;
2462 server.open_file(bar_uri.clone(), bar.program())?;
2463
2464 let cases = [
2465 (&foo, &foo_uri, "root"),
2466 (&foo, &foo_uri, "foobar"),
2467 (&foo, &foo_uri, "foo"),
2468 (&foo, &foo_uri, "bar"),
2469 (&foo, &foo_uri, "baz"),
2470 (&foo, &foo_uri, "quz"),
2471 (&bar, &bar_uri, "x"),
2472 (&bar, &bar_uri, "y"),
2473 ];
2474
2475 let expected_results = cases
2476 .iter()
2477 .map(|(fixture, uri, id)| {
2478 expected_location_link_from_spans(
2479 (*uri).clone(),
2480 foo.resolved_span(id),
2481 fixture.resolved_span(&format!("dest_{}", id)),
2482 )
2483 })
2484 .collect::<Vec<_>>();
2485
2486 let requests = cases
2487 .iter()
2488 .map(|(_, _, id)| {
2489 goto_definition_request(
2490 &mut server,
2491 foo_uri.clone(),
2492 foo.begin_line(&format!("{}_click", id)),
2493 foo.begin_column(&format!("{}_click", id)),
2494 )
2495 })
2496 .collect::<Vec<_>>();
2497
2498 for (case, request, expected) in itertools::izip!(cases, requests, expected_results) {
2499 let req_id = server.send_request(request)?;
2500 let response = goto_definition_response_location(&mut server, req_id)
2501 .context(format!("getting response for case `{}`", case.2))?;
2502
2503 assert_eq!(
2504 expected, response,
2505 "Incorrect response for case `{}`",
2506 case.2
2507 );
2508 }
2509 Ok(())
2510 }
2511}