starlark_lsp/
server.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! Based on the reference lsp-server example at <https://github.com/rust-analyzer/lsp-server/blob/master/examples/goto_def.rs>.
19
20use 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
112/// The request to get the file contents for a starlark: URI
113struct 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/// Params to get the file contents for a starlark: URI.
122#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
123#[serde(rename_all = "camelCase")] // camelCase to match idioms in LSP spec / typescript land.
124struct StarlarkFileContentsParams {
125    uri: LspUrl,
126}
127
128/// The contents of a starlark: URI if available.
129#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
130#[serde(rename_all = "camelCase")] // camelCase to match idioms in LSP spec / typescript land.
131struct StarlarkFileContentsResponse {
132    contents: Option<String>,
133}
134
135/// Errors that can happen when converting LspUrl and Url to/from each other.
136#[derive(thiserror::Error, Debug)]
137pub enum LspUrlError {
138    /// The path component of the URL was not absolute. This is required for all supported
139    /// URL types.
140    #[error("`{}` does not have an absolute path component", .0)]
141    NotAbsolute(Url),
142    /// For some reason the PathBuf/Url in the LspUrl could not be converted back to a URL.
143    #[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/// A URL that represents the two types (plus an "Other") of URIs that are supported.
150#[derive(Clone, Debug, Hash, Eq, PartialEq, Display)]
151pub enum LspUrl {
152    /// A "file://" url with a path sent from the LSP client.
153    #[display("file://{}", _0.display())]
154    File(PathBuf),
155    /// A "starlark:" url. This is mostly used for native types that don't actually
156    /// exist on the filesystem. The path component always has a leading slash.
157    #[display("starlark:{}", _0.display())]
158    Starlark(PathBuf),
159    /// Any other type. Often should just be ignored, or return an error.
160    #[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    /// Returns the path component of the underlying URL
188    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                // Use "starts with a /" because, while leading slashes are accepted on
216                // windows, they do not report "true" from `is_absolute()`.
217                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/// The result of resolving a StringLiteral when looking up a definition.
252#[derive(Derivative)]
253#[derivative(Debug)]
254pub struct StringLiteralResult {
255    /// The path that a string literal resolves to.
256    pub url: LspUrl,
257    /// A function that takes the AstModule at path specified by `url`, and
258    /// allows resolving a location to jump to within the specific URL if desired.
259    ///
260    /// If `None`, then just jump to the URL. Do not attempt to load the file.
261    #[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/// The result of evaluating a starlark program for use in the LSP.
271#[derive(Default)]
272pub struct LspEvalResult {
273    /// The list of diagnostic issues that were encountered while evaluating a starlark program.
274    pub diagnostics: Vec<Diagnostic>,
275    /// If the program could be parsed, the parsed module.
276    pub ast: Option<AstModule>,
277}
278
279/// Settings that the LspContext can provide to change what capabilities the server enables
280/// or disables.
281#[derive(Dupe, Clone, Debug, serde::Serialize, serde::Deserialize)]
282pub struct LspServerSettings {
283    /// Whether goto definition should work.
284    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
295/// Various pieces of context to allow the LSP to interact with starlark parsers, etc.
296pub trait LspContext {
297    /// Parse a file with the given contents. The filename is used in the diagnostics.
298    fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult;
299
300    /// Resolve a path given in a `load()` statement.
301    ///
302    /// `path` is the string representation in the `load()` statement. Its meaning is
303    ///        implementation defined.
304    /// `current_file` is the the file that is including the `load()` statement, and should be used
305    ///                if `path` is "relative" in a semantic sense.
306    fn resolve_load(
307        &self,
308        path: &str,
309        current_file: &LspUrl,
310        workspace_root: Option<&Path>,
311    ) -> anyhow::Result<LspUrl>;
312
313    /// Render the target URL to use as a path in a `load()` statement. If `target` is
314    /// in the same package as `current_file`, the result is a relative path.
315    ///
316    /// `target` is the file that should be loaded by `load()`.
317    /// `current_file` is the file that the `load()` statement will be inserted into.
318    fn render_as_load(
319        &self,
320        target: &LspUrl,
321        current_file: &LspUrl,
322        workspace_root: Option<&Path>,
323    ) -> anyhow::Result<String>;
324
325    /// Resolve a string literal into a Url and a function that specifies a location within that
326    /// target file.
327    ///
328    /// This can be used for things like file paths in string literals, build targets, etc.
329    ///
330    /// `current_file` is the file that is currently being evaluated
331    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    /// Get the contents of a starlark program at a given path, if it exists.
339    fn get_load_contents(&self, uri: &LspUrl) -> anyhow::Result<Option<String>>;
340
341    /// Get the contents of a file at a given URI, and attempt to parse it.
342    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    /// Get the preloaded environment for a particular file.
350    fn get_environment(&self, uri: &LspUrl) -> DocModule;
351
352    /// Get the LSPUrl for a global symbol if possible.
353    ///
354    /// The current file is provided in case different files have different global symbols
355    /// defined.
356    fn get_url_for_global_symbol(
357        &self,
358        current_file: &LspUrl,
359        symbol: &str,
360    ) -> anyhow::Result<Option<LspUrl>>;
361
362    /// Get valid completion options if possible, based on the kind of string
363    /// completion expected (e.g. any string literal, versus the path argument in
364    /// a load statement).
365    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/// Errors when [`LspContext::resolve_load()`] cannot resolve a given path.
378#[derive(thiserror::Error, Debug)]
379enum ResolveLoadError {
380    /// The scheme provided was not correct or supported.
381    #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
382    WrongScheme(String, LspUrl),
383}
384
385/// Errors when loading contents of a starlark program.
386#[derive(thiserror::Error, Debug)]
387pub(crate) enum LoadContentsError {
388    /// The scheme provided was not correct or supported.
389    #[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    /// The `AstModule` from the last time that a file was opened / changed and parsed successfully.
397    /// Entries are evicted when the file is closed.
398    pub(crate) last_valid_parse: RwLock<HashMap<LspUrl, Arc<LspModule>>>,
399}
400
401/// The logic implementations of stuff
402impl<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        // We asked for Sync full, so can just grab all the text from params
461        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(&params.text_document.uri.clone().try_into()?);
473        }
474        self.publish_diagnostics(params.text_document.uri, Vec::new(), None);
475        Ok(())
476    }
477
478    /// Go to the definition of the symbol at the current cursor if that definition is in
479    /// the same file.
480    ///
481    /// NOTE: This uses the last valid parse of a file as a basis for symbol locations.
482    /// If a file has changed and does result in a valid parse, then symbol locations may
483    /// be slightly incorrect.
484    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    /// Offers completion of known symbols in the current file.
497    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    /// Offers hover information for the symbol at the current cursor.
510    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    /// Get the file contents of a starlark: URI.
515    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(&params.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    /// Simple helper to generate `Some(LocationLink)` objects in `resolve_definition_location`
541    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    /// Find the ultimate places that an identifier is defined.
555    ///
556    /// Takes a definition location and if necessary loads other files trying
557    /// to find where the symbol was defined in a useful way. e.g. pointing to the
558    /// symbol in a "load()" statement isn't useful, but going to the file it is
559    /// loaded from and pointing at a function definition very much is.
560    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                        // If there's an error loading the file to parse it, at least
613                        // try to get to the file.
614                        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                    // In this case we don't pass the name along in the root_definition_location,
688                    // so it's simpler to do the lookup here, rather than threading a ton of
689                    // information through.
690                    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                // Figure out what kind of position we are in, to determine the best type of
740                // autocomplete.
741                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    /// Using all currently loaded documents, gather a list of known exported
817    /// symbols. This list contains both the symbols exported from the loaded
818    /// files, as well as symbols loaded in the open files. Symbols that are
819    /// loaded from modules that are open are deduplicated.
820    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                // We're already loading a symbol from this module path, construct
942                // a text edit that amends the existing load.
943                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                // We're not yet loading from this module, construct a text edit that
972                // inserts a new load statement after the last one we found.
973                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    /// Get completion items for each language keyword.
992    pub(crate) fn get_keyword_completion_items() -> impl Iterator<Item = CompletionItem> {
993        [
994            // Actual keywords
995            "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    /// Get hover information for a given position in a document.
1007    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        // Return an empty result as a "not found"
1023        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                        // Not something we really support yet, so just provide hover information for
1044                        // the root definition.
1045                        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                // TODO: This seems very inefficient. Once the document starts
1073                // holding the `Scope` including AST nodes, this indirection
1074                // should be removed.
1075                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                // Symbol loaded from another file. Find the file and get the definition
1107                // from there, hopefully including the docs.
1108                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                    // We might just be hovering a string that's not a file/target/etc,
1126                    // so just return nothing.
1127                    return Ok(None);
1128                };
1129                match resolved_literal {
1130                    Some(StringLiteralResult {
1131                        url,
1132                        location_finder: Some(location_finder),
1133                    }) => {
1134                        // If there's an error loading the file to parse it, at least
1135                        // try to get to the file.
1136                        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                // Try to resolve as a global symbol.
1158                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
1190/// The library style pieces
1191impl<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                    // TODO(nmj): Also implement DocumentSymbols so that some logic can
1222                    //            be handled client side.
1223                    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                    // Currently don't handle any other requests
1235                }
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                    // Don't expect any of these
1247                }
1248            }
1249        }
1250        Ok(())
1251    }
1252}
1253
1254/// Instantiate an LSP server that reads on stdin, and writes to stdout
1255pub fn stdio_server<T: LspContext>(context: T) -> anyhow::Result<()> {
1256    // Note that  we must have our logging only write out to stderr.
1257    eprintln!("Starting Rust Starlark server");
1258
1259    let (connection, io_threads) = Connection::stdio();
1260    server_with_connection(connection, context)?;
1261    // Make sure that the io threads stop properly too.
1262    io_threads.join()?;
1263
1264    eprintln!("Stopping Rust Starlark server");
1265    Ok(())
1266}
1267
1268/// Instantiate an LSP server that reads and writes using the given connection.
1269pub fn server_with_connection<T: LspContext>(
1270    connection: Connection,
1271    context: T,
1272) -> anyhow::Result<()> {
1273    // Run the server and wait for the main thread to end (typically by trigger LSP Exit event).
1274    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
1336/// Create a new `Notification` object with the correct name from the given params.
1337pub(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(&params).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    // Converts PathBuf to string that can be used in starlark load statements within "" quotes.
1476    // Replaces \ with / (for Windows paths).
1477    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        // Initialize with "junk" on disk so that we make sure we're using the contents from the
1605        // client (potentially indicating an unsaved, modified file)
1606        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        // File with broken syntax
2164        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        // File that is not starlark at all
2182        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        // Directory that doesn't look like a starlark file.
2200        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        // Directory that looks like a starlark file by name, but isn't
2218        // File that is not starlark at all
2219        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}