/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Based on the reference lsp-server example at <https://github.com/rust-analyzer/lsp-server/blob/master/examples/goto_def.rs>.

use std::collections::HashMap;
use std::fmt::Debug;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;

use derivative::Derivative;
use derive_more::Display;
use dupe::Dupe;
use dupe::OptionDupedExt;
use lsp_server::Connection;
use lsp_server::Message;
use lsp_server::Notification;
use lsp_server::Request;
use lsp_server::RequestId;
use lsp_server::Response;
use lsp_server::ResponseError;
use lsp_types::notification::DidChangeTextDocument;
use lsp_types::notification::DidCloseTextDocument;
use lsp_types::notification::DidOpenTextDocument;
use lsp_types::notification::LogMessage;
use lsp_types::notification::PublishDiagnostics;
use lsp_types::request::GotoDefinition;
use lsp_types::DefinitionOptions;
use lsp_types::Diagnostic;
use lsp_types::DidChangeTextDocumentParams;
use lsp_types::DidCloseTextDocumentParams;
use lsp_types::DidOpenTextDocumentParams;
use lsp_types::GotoDefinitionParams;
use lsp_types::GotoDefinitionResponse;
use lsp_types::InitializeParams;
use lsp_types::LocationLink;
use lsp_types::LogMessageParams;
use lsp_types::MessageType;
use lsp_types::OneOf;
use lsp_types::PublishDiagnosticsParams;
use lsp_types::Range;
use lsp_types::ServerCapabilities;
use lsp_types::TextDocumentSyncCapability;
use lsp_types::TextDocumentSyncKind;
use lsp_types::Url;
use lsp_types::WorkDoneProgressOptions;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;

use crate::analysis::definition::Definition;
use crate::analysis::definition::DottedDefinition;
use crate::analysis::definition::IdentifierDefinition;
use crate::analysis::definition::LspModule;
use crate::codemap::ResolvedSpan;
use crate::lsp::server::LoadContentsError::WrongScheme;
use crate::syntax::AstModule;

/// The request to get the file contents for a starlark: URI
struct StarlarkFileContentsRequest {}

impl lsp_types::request::Request for StarlarkFileContentsRequest {
    type Params = StarlarkFileContentsParams;
    type Result = StarlarkFileContentsResponse;
    const METHOD: &'static str = "starlark/fileContents";
}

/// Params to get the file contents for a starlark: URI.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] // camelCase to match idioms in LSP spec / typescript land.
struct StarlarkFileContentsParams {
    uri: LspUrl,
}

/// The contents of a starlark: URI if available.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] // camelCase to match idioms in LSP spec / typescript land.
struct StarlarkFileContentsResponse {
    contents: Option<String>,
}

/// Errors that can happen when converting LspUrl and Url to/from each other.
#[derive(thiserror::Error, Debug)]
pub enum LspUrlError {
    /// The path component of the URL was not absolute. This is required for all supported
    /// URL types.
    #[error("`{}` does not have an absolute path component", .0)]
    NotAbsolute(Url),
    /// For some reason the PathBuf/Url in the LspUrl could not be converted back to a URL.
    #[error("`{}` could not be converted back to a URL", .0)]
    Unparsable(LspUrl),
}

/// A URL that represents the two types (plus an "Other") of URIs that are supported.
#[derive(Clone, Debug, Hash, Eq, PartialEq, Display)]
pub enum LspUrl {
    /// A "file://" url with a path sent from the LSP client.
    #[display(fmt = "file://{}", "_0.display()")]
    File(PathBuf),
    /// A "starlark:" url. This is mostly used for native types that don't actually
    /// exist on the filesystem. The path component always has a leading slash.
    #[display(fmt = "starlark:{}", "_0.display()")]
    Starlark(PathBuf),
    /// Any other type. Often should just be ignored, or return an error.
    #[display(fmt = "{}", "_0")]
    Other(Url),
}

impl Serialize for LspUrl {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match Url::try_from(self) {
            Ok(url) => url.serialize(serializer),
            Err(e) => Err(serde::ser::Error::custom(e.to_string())),
        }
    }
}

impl<'de> Deserialize<'de> for LspUrl {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let url = Url::deserialize(deserializer)?;
        LspUrl::try_from(url).map_err(|e| serde::de::Error::custom(e.to_string()))
    }
}

impl LspUrl {
    /// Returns the path component of the underlying URL
    pub fn path(&self) -> &Path {
        match self {
            LspUrl::File(p) => p.as_path(),
            LspUrl::Starlark(p) => p.as_path(),
            LspUrl::Other(u) => Path::new(u.path()),
        }
    }
}

impl TryFrom<Url> for LspUrl {
    type Error = LspUrlError;

    fn try_from(url: Url) -> Result<Self, Self::Error> {
        match url.scheme() {
            "file" => {
                let path = PathBuf::from(url.path());
                if path.to_string_lossy().starts_with('/') {
                    Ok(Self::File(path))
                } else {
                    Err(LspUrlError::NotAbsolute(url))
                }
            }
            "starlark" => {
                let path = PathBuf::from(url.path());
                // Use "starts with a /" because, while leading slashes are accepted on
                // windows, they do not report "true" from `is_absolute()`.
                if path.to_string_lossy().starts_with('/') {
                    Ok(Self::Starlark(path))
                } else {
                    Err(LspUrlError::NotAbsolute(url))
                }
            }
            _ => Ok(Self::Other(url)),
        }
    }
}

impl TryFrom<LspUrl> for Url {
    type Error = LspUrlError;

    fn try_from(url: LspUrl) -> Result<Self, Self::Error> {
        Url::try_from(&url)
    }
}

impl TryFrom<&LspUrl> for Url {
    type Error = LspUrlError;

    fn try_from(url: &LspUrl) -> Result<Self, Self::Error> {
        match &url {
            LspUrl::File(p) => {
                Url::from_file_path(p).map_err(|_| LspUrlError::Unparsable(url.clone()))
            }
            LspUrl::Starlark(p) => Url::parse(&format!("starlark:{}", p.display()))
                .map_err(|_| LspUrlError::Unparsable(url.clone())),
            LspUrl::Other(u) => Ok(u.clone()),
        }
    }
}

/// The result of resolving a StringLiteral when looking up a definition.
#[derive(Derivative)]
#[derivative(Debug)]
pub struct StringLiteralResult {
    /// The path that a string literal resolves to.
    pub url: LspUrl,
    /// A function that takes the AstModule at path specified by `url` and that same url, and
    /// allows resolving a location to jump to within the specific URL if desired.
    ///
    /// If `None`, then just jump to the URL. Do not attempt to load the file.
    #[derivative(Debug = "ignore")]
    pub location_finder:
        Option<Box<dyn FnOnce(&AstModule, &LspUrl) -> anyhow::Result<Option<Range>> + Send>>,
}

fn _assert_string_literal_result_is_send() {
    fn assert_send<T: Send>() {}
    assert_send::<StringLiteralResult>();
}

/// The result of evaluating a starlark program for use in the LSP.
#[derive(Default)]
pub struct LspEvalResult {
    /// The list of diagnostic issues that were encountered while evaluating a starlark program.
    pub diagnostics: Vec<Diagnostic>,
    /// If the program could be parsed, the parsed module.
    pub ast: Option<AstModule>,
}

/// Settings that the LspContext can provide to change what capabilities the server enables
/// or disables.
#[derive(Dupe, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct LspServerSettings {
    /// Whether goto definition should work.
    pub enable_goto_definition: bool,
}

impl Default for LspServerSettings {
    fn default() -> Self {
        Self {
            enable_goto_definition: true,
        }
    }
}

/// Various pieces of context to allow the LSP to interact with starlark parsers, etc.
pub trait LspContext {
    /// Parse a file with the given contents. The filename is used in the diagnostics.
    fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult;

    /// Resolve a path given in a `load()` statement.
    ///
    /// `path` is the string representation in the `load()` statement. Its meaning is
    ///        implementation defined.
    /// `current_file` is the the file that is including the `load()` statement, and should be used
    ///                if `path` is "relative" in a semantic sense.
    fn resolve_load(&self, path: &str, current_file: &LspUrl) -> anyhow::Result<LspUrl>;

    /// Resolve a string literal into a Url and a function that specifies a locaction within that
    /// target file.
    ///
    /// This can be used for things like file paths in string literals, build targets, etc.
    ///
    /// `current_file` is the file that is currently being evaluated
    fn resolve_string_literal(
        &self,
        literal: &str,
        current_file: &LspUrl,
    ) -> anyhow::Result<Option<StringLiteralResult>>;

    /// Get the contents of a starlark program at a given path, if it exists.
    fn get_load_contents(&self, uri: &LspUrl) -> anyhow::Result<Option<String>>;

    /// Get the contents of a file at a given URI, and attempt to parse it.
    fn parse_file(&self, uri: &LspUrl) -> anyhow::Result<Option<LspEvalResult>> {
        let result = self
            .get_load_contents(uri)?
            .map(|content| self.parse_file_with_contents(uri, content));
        Ok(result)
    }

    /// Get the LSPUrl for a global symbol if possible.
    ///
    /// The current file is provided in case different files have different global symbols
    /// defined.
    fn get_url_for_global_symbol(
        &self,
        current_file: &LspUrl,
        symbol: &str,
    ) -> anyhow::Result<Option<LspUrl>>;
}

/// Errors when [`LspContext::resolve_load()`] cannot resolve a given path.
#[derive(thiserror::Error, Debug)]
enum ResolveLoadError {
    /// The scheme provided was not correct or supported.
    #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
    WrongScheme(String, LspUrl),
}

/// Errors when loading contents of a starlark program.
#[derive(thiserror::Error, Debug)]
pub(crate) enum LoadContentsError {
    /// The scheme provided was not correct or supported.
    #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
    WrongScheme(String, LspUrl),
}

struct Backend<T: LspContext> {
    connection: Connection,
    context: T,
    /// The `AstModule` from the last time that a file was opened / changed and parsed successfully.
    /// Entries are evicted when the file is closed.
    last_valid_parse: RwLock<HashMap<LspUrl, Arc<LspModule>>>,
}

/// The logic implementations of stuff
impl<T: LspContext> Backend<T> {
    fn server_capabilities(settings: LspServerSettings) -> ServerCapabilities {
        let definition_provider = settings.enable_goto_definition.then_some({
            OneOf::Right(DefinitionOptions {
                work_done_progress_options: WorkDoneProgressOptions {
                    work_done_progress: None,
                },
            })
        });
        ServerCapabilities {
            text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
            definition_provider,
            ..ServerCapabilities::default()
        }
    }

    fn get_ast(&self, uri: &LspUrl) -> Option<Arc<LspModule>> {
        let last_valid_parse = self.last_valid_parse.read().unwrap();
        last_valid_parse.get(uri).duped()
    }

    fn get_ast_or_load_from_disk(&self, uri: &LspUrl) -> anyhow::Result<Option<Arc<LspModule>>> {
        let module = match self.get_ast(uri) {
            Some(result) => Some(result),
            None => self
                .context
                .parse_file(uri)?
                .and_then(|eval_result| eval_result.ast.map(|ast| Arc::new(LspModule::new(ast)))),
        };
        Ok(module)
    }

    fn validate(&self, uri: Url, version: Option<i64>, text: String) -> anyhow::Result<()> {
        let uri = uri.try_into()?;
        let eval_result = self.context.parse_file_with_contents(&uri, text);
        if let Some(ast) = eval_result.ast {
            let module = Arc::new(LspModule::new(ast));
            let mut last_valid_parse = self.last_valid_parse.write().unwrap();
            last_valid_parse.insert(uri.clone(), module);
        }
        self.publish_diagnostics(uri.try_into()?, eval_result.diagnostics, version);
        Ok(())
    }

    fn did_open(&self, params: DidOpenTextDocumentParams) -> anyhow::Result<()> {
        self.validate(
            params.text_document.uri,
            Some(params.text_document.version as i64),
            params.text_document.text,
        )
    }

    fn did_change(&self, params: DidChangeTextDocumentParams) -> anyhow::Result<()> {
        // We asked for Sync full, so can just grab all the text from params
        let change = params.content_changes.into_iter().next().unwrap();
        self.validate(
            params.text_document.uri,
            Some(params.text_document.version as i64),
            change.text,
        )
    }

    fn did_close(&self, params: DidCloseTextDocumentParams) -> anyhow::Result<()> {
        {
            let mut last_valid_parse = self.last_valid_parse.write().unwrap();
            last_valid_parse.remove(&params.text_document.uri.clone().try_into()?);
        }
        self.publish_diagnostics(params.text_document.uri, Vec::new(), None);
        Ok(())
    }

    /// Go to the definition of the symbol at the current cursor if that definition is in
    /// the same file.
    ///
    /// NOTE: This uses the last valid parse of a file as a basis for symbol locations.
    /// If a file has changed and does result in a valid parse, then symbol locations may
    /// be slightly incorrect.
    fn goto_definition(&self, id: RequestId, params: GotoDefinitionParams) {
        self.send_response(new_response(id, self.find_definition(params)));
    }

    /// Get the file contents of a starlark: URI.
    fn get_starlark_file_contents(&self, id: RequestId, params: StarlarkFileContentsParams) {
        let response: anyhow::Result<_> = match params.uri {
            LspUrl::Starlark(_) => self
                .context
                .get_load_contents(&params.uri)
                .map(|contents| StarlarkFileContentsResponse { contents }),
            _ => Err(WrongScheme("starlark:".to_owned(), params.uri).into()),
        };
        self.send_response(new_response(id, response));
    }

    fn resolve_load_path(&self, path: &str, current_uri: &LspUrl) -> anyhow::Result<LspUrl> {
        match current_uri {
            LspUrl::File(_) => self.context.resolve_load(path, current_uri),
            LspUrl::Starlark(_) | LspUrl::Other(_) => {
                Err(ResolveLoadError::WrongScheme("file://".to_owned(), current_uri.clone()).into())
            }
        }
    }

    /// Simple helper to generate `Some(LocationLink)` objects in `resolve_definition_location`
    fn location_link<R: Into<Range> + Copy>(
        source: ResolvedSpan,
        uri: &LspUrl,
        target_range: R,
    ) -> anyhow::Result<Option<LocationLink>> {
        Ok(Some(LocationLink {
            origin_selection_range: Some(source.into()),
            target_uri: uri.try_into()?,
            target_range: target_range.into(),
            target_selection_range: target_range.into(),
        }))
    }

    /// Find the ultimate places that an identifier is defined.
    ///
    /// Takes a definition location and if necessary loads other files trying
    /// to find where the symbol was defined in a useful way. e.g. pointing to the
    /// symbol in a "load()" statement isn't useful, but going to the file it is
    /// loaded from and pointing at a function definition very much is.
    fn resolve_definition_location(
        &self,
        definition: IdentifierDefinition,
        source: ResolvedSpan,
        member: Option<&str>,
        uri: LspUrl,
    ) -> anyhow::Result<Option<LocationLink>> {
        let ret = match definition {
            IdentifierDefinition::Location {
                destination: target,
                ..
            } => Self::location_link(source, &uri, target)?,
            IdentifierDefinition::LoadedLocation {
                destination: location,
                path,
                name,
                ..
            } => {
                let load_uri = self.resolve_load_path(&path, &uri)?;
                let loaded_location =
                    self.get_ast_or_load_from_disk(&load_uri)?
                        .and_then(|ast| match member {
                            Some(member) => ast.find_exported_symbol_and_member(&name, member),
                            None => ast.find_exported_symbol(&name),
                        });
                match loaded_location {
                    None => Self::location_link(source, &uri, location)?,
                    Some(loaded_location) => {
                        Self::location_link(source, &load_uri, loaded_location)?
                    }
                }
            }
            IdentifierDefinition::NotFound => None,
            IdentifierDefinition::LoadPath { path, .. } => {
                match self.resolve_load_path(&path, &uri) {
                    Ok(load_uri) => Self::location_link(source, &load_uri, Range::default())?,
                    Err(_) => None,
                }
            }
            IdentifierDefinition::StringLiteral { literal, .. } => {
                let literal = self.context.resolve_string_literal(&literal, &uri)?;
                match literal {
                    Some(StringLiteralResult {
                        url,
                        location_finder: Some(location_finder),
                    }) => {
                        // If there's an error loading the file to parse it, at least
                        // try to get to the file.
                        let result =
                            self.get_ast_or_load_from_disk(&url)
                                .and_then(|ast| match ast {
                                    Some(module) => location_finder(&module.ast, &url),
                                    None => Ok(None),
                                });
                        if let Err(e) = &result {
                            eprintln!("Error jumping to definition: {:#}", e);
                        }
                        let target_range = result.unwrap_or_default().unwrap_or_default();
                        Self::location_link(source, &url, target_range)?
                    }
                    Some(StringLiteralResult {
                        url,
                        location_finder: None,
                    }) => Self::location_link(source, &url, Range::default())?,
                    _ => None,
                }
            }
            IdentifierDefinition::Unresolved { name, .. } => {
                match self.context.get_url_for_global_symbol(&uri, &name)? {
                    Some(uri) => {
                        let loaded_location =
                            self.get_ast_or_load_from_disk(&uri)?
                                .and_then(|ast| match member {
                                    Some(member) => {
                                        ast.find_exported_symbol_and_member(&name, member)
                                    }
                                    None => ast.find_exported_symbol(&name),
                                });

                        Self::location_link(source, &uri, loaded_location.unwrap_or_default())?
                    }
                    None => None,
                }
            }
        };
        Ok(ret)
    }

    fn find_definition(
        &self,
        params: GotoDefinitionParams,
    ) -> anyhow::Result<GotoDefinitionResponse> {
        let uri = params
            .text_document_position_params
            .text_document
            .uri
            .try_into()?;
        let line = params.text_document_position_params.position.line;
        let character = params.text_document_position_params.position.character;

        let location = match self.get_ast(&uri) {
            Some(ast) => {
                let location = ast.find_definition(line, character);
                let source = location.source().unwrap_or_default();
                match location {
                    Definition::Identifier(definition) => {
                        self.resolve_definition_location(definition, source, None, uri)?
                    }
                    // In this case we don't pass the name along in the root_definition_location,
                    // so it's simpler to do the lookup here, rather than threading a ton of
                    // information through.
                    Definition::Dotted(DottedDefinition {
                        root_definition_location: IdentifierDefinition::Location { .. },
                        segments,
                        ..
                    }) => {
                        let member_location = ast.find_exported_symbol_and_member(
                            segments.first().expect("at least one segment").as_str(),
                            segments.get(1).expect("at least two segments").as_str(),
                        );
                        Self::location_link(source, &uri, member_location.unwrap_or_default())?
                    }
                    Definition::Dotted(definition) => self.resolve_definition_location(
                        definition.root_definition_location,
                        source,
                        Some(
                            definition
                                .segments
                                .last()
                                .expect("to have at least one component")
                                .as_str(),
                        ),
                        uri,
                    )?,
                }
            }
            None => None,
        };

        let response = match location {
            Some(location) => vec![location],
            None => vec![],
        };
        Ok(GotoDefinitionResponse::Link(response))
    }
}

/// The library style pieces
impl<T: LspContext> Backend<T> {
    fn send_notification(&self, x: Notification) {
        self.connection
            .sender
            .send(Message::Notification(x))
            .unwrap()
    }

    fn send_response(&self, x: Response) {
        self.connection.sender.send(Message::Response(x)).unwrap()
    }

    fn log_message(&self, typ: MessageType, message: &str) {
        self.send_notification(new_notification::<LogMessage>(LogMessageParams {
            typ,
            message: message.to_owned(),
        }))
    }

    fn publish_diagnostics(&self, uri: Url, diags: Vec<Diagnostic>, version: Option<i64>) {
        self.send_notification(new_notification::<PublishDiagnostics>(
            PublishDiagnosticsParams::new(uri, diags, version.map(|i| i as i32)),
        ));
    }

    fn main_loop(&self, _params: InitializeParams) -> anyhow::Result<()> {
        self.log_message(MessageType::INFO, "Starlark server initialised");
        for msg in &self.connection.receiver {
            match msg {
                Message::Request(req) => {
                    // TODO(nmj): Also implement DocumentSymbols so that some logic can
                    //            be handled client side.
                    if let Some(params) = as_request::<GotoDefinition>(&req) {
                        self.goto_definition(req.id, params);
                    } else if let Some(params) = as_request::<StarlarkFileContentsRequest>(&req) {
                        self.get_starlark_file_contents(req.id, params);
                    } else if self.connection.handle_shutdown(&req)? {
                        return Ok(());
                    }
                    // Currently don't handle any other requests
                }
                Message::Notification(x) => {
                    if let Some(params) = as_notification::<DidOpenTextDocument>(&x) {
                        self.did_open(params)?;
                    } else if let Some(params) = as_notification::<DidChangeTextDocument>(&x) {
                        self.did_change(params)?;
                    } else if let Some(params) = as_notification::<DidCloseTextDocument>(&x) {
                        self.did_close(params)?;
                    }
                }
                Message::Response(_) => {
                    // Don't expect any of these
                }
            }
        }
        Ok(())
    }
}

/// Instantiate an LSP server that reads on stdin, and writes to stdout
pub fn stdio_server<T: LspContext>(context: T) -> anyhow::Result<()> {
    // Note that  we must have our logging only write out to stderr.
    eprintln!("Starting Rust Starlark server");

    let (connection, io_threads) = Connection::stdio();
    server_with_connection(connection, context)?;
    // Make sure that the io threads stop properly too.
    io_threads.join()?;

    eprintln!("Stopping Rust Starlark server");
    Ok(())
}

/// Instantiate an LSP server that reads and writes using the given connection.
pub fn server_with_connection<T: LspContext>(
    connection: Connection,
    context: T,
) -> anyhow::Result<()> {
    // Run the server and wait for the main thread to end (typically by trigger LSP Exit event).
    let (init_request_id, init_value) = connection.initialize_start()?;

    let initialization_params: InitializeParams = serde_json::from_value(init_value)?;
    let server_settings = initialization_params
        .initialization_options
        .as_ref()
        .and_then(|opts| serde_json::from_value(opts.clone()).ok())
        .unwrap_or_default();
    let capabilities_payload = Backend::<T>::server_capabilities(server_settings);
    let server_capabilities = serde_json::to_value(capabilities_payload).unwrap();

    let initialize_data = serde_json::json!({
            "capabilities": server_capabilities,
    });
    connection.initialize_finish(init_request_id, initialize_data)?;

    Backend {
        connection,
        context,
        last_valid_parse: RwLock::default(),
    }
    .main_loop(initialization_params)?;

    Ok(())
}

fn as_notification<T>(x: &Notification) -> Option<T::Params>
where
    T: lsp_types::notification::Notification,
    T::Params: DeserializeOwned,
{
    if x.method == T::METHOD {
        let params = serde_json::from_value(x.params.clone()).unwrap_or_else(|err| {
            panic!(
                "Invalid notification\nMethod: {}\n error: {}",
                x.method, err
            )
        });
        Some(params)
    } else {
        None
    }
}

fn as_request<T>(x: &Request) -> Option<T::Params>
where
    T: lsp_types::request::Request,
    T::Params: DeserializeOwned,
{
    if x.method == T::METHOD {
        let params = serde_json::from_value(x.params.clone()).unwrap_or_else(|err| {
            panic!(
                "Invalid request\n  method: {}\n  error: {}\n  request: {:?}\n",
                x.method, err, x
            )
        });
        Some(params)
    } else {
        None
    }
}

/// Create a new `Notification` object with the correct name from the given params.
pub(crate) fn new_notification<T>(params: T::Params) -> Notification
where
    T: lsp_types::notification::Notification,
{
    Notification {
        method: T::METHOD.to_owned(),
        params: serde_json::to_value(&params).unwrap(),
    }
}

fn new_response<T>(id: RequestId, params: anyhow::Result<T>) -> Response
where
    T: serde::Serialize,
{
    match params {
        Ok(params) => Response {
            id,
            result: Some(serde_json::to_value(params).unwrap()),
            error: None,
        },
        Err(e) => Response {
            id,
            result: None,
            error: Some(ResponseError {
                code: 0,
                message: format!("{:#?}", e),
                data: None,
            }),
        },
    }
}

// TODO(nmj): Some of the windows tests get a bit flaky, especially around
//            some paths. Revisit later.
#[cfg(all(test, not(windows)))]
mod test {
    use std::path::Path;
    use std::path::PathBuf;

    use anyhow::Context;
    use lsp_server::Request;
    use lsp_server::RequestId;
    use lsp_types::request::GotoDefinition;
    use lsp_types::GotoDefinitionParams;
    use lsp_types::GotoDefinitionResponse;
    use lsp_types::LocationLink;
    use lsp_types::Position;
    use lsp_types::Range;
    use lsp_types::TextDocumentIdentifier;
    use lsp_types::TextDocumentPositionParams;
    use lsp_types::Url;
    use textwrap::dedent;

    use crate::analysis::definition::helpers::FixtureWithRanges;
    use crate::codemap::ResolvedSpan;
    use crate::lsp::server::LspServerSettings;
    use crate::lsp::server::LspUrl;
    use crate::lsp::server::StarlarkFileContentsParams;
    use crate::lsp::server::StarlarkFileContentsRequest;
    use crate::lsp::server::StarlarkFileContentsResponse;
    use crate::lsp::test::TestServer;
    use crate::wasm::is_wasm;

    fn goto_definition_request(
        server: &mut TestServer,
        uri: Url,
        line: u32,
        character: u32,
    ) -> Request {
        server.new_request::<GotoDefinition>(GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri },
                position: Position { line, character },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        })
    }

    fn goto_definition_response_location(
        server: &mut TestServer,
        request_id: RequestId,
    ) -> anyhow::Result<LocationLink> {
        let response = server.get_response::<GotoDefinitionResponse>(request_id)?;
        match response {
            GotoDefinitionResponse::Link(locations) if locations.len() == 1 => {
                Ok(locations[0].clone())
            }
            _ => Err(anyhow::anyhow!("Got invalid message type: {:?}", response)),
        }
    }

    fn expected_location_link(
        uri: Url,
        source_line: u32,
        source_start_col: u32,
        source_end_col: u32,
        dest_line: u32,
        dest_start_col: u32,
        dest_end_col: u32,
    ) -> LocationLink {
        let source_range = Range::new(
            Position::new(source_line, source_start_col),
            Position::new(source_line, source_end_col),
        );
        let dest_range = Range::new(
            Position::new(dest_line, dest_start_col),
            Position::new(dest_line, dest_end_col),
        );
        LocationLink {
            origin_selection_range: Some(source_range),
            target_uri: uri,
            target_range: dest_range,
            target_selection_range: dest_range,
        }
    }

    fn expected_location_link_from_spans(
        uri: Url,
        source_span: ResolvedSpan,
        dest_span: ResolvedSpan,
    ) -> LocationLink {
        LocationLink {
            origin_selection_range: Some(source_span.into()),
            target_uri: uri,
            target_range: dest_span.into(),
            target_selection_range: dest_span.into(),
        }
    }

    #[cfg(windows)]
    fn temp_file_uri(rel_path: &str) -> Url {
        Url::from_file_path(&PathBuf::from("C:/tmp").join(rel_path)).unwrap()
    }

    #[cfg(not(windows))]
    fn temp_file_uri(rel_path: &str) -> Url {
        Url::from_file_path(PathBuf::from("/tmp").join(rel_path)).unwrap()
    }

    #[test]
    fn sends_empty_goto_definition_on_nonexistent_file() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let mut server = TestServer::new()?;
        let req = goto_definition_request(&mut server, temp_file_uri("nonexistent"), 0, 0);

        let request_id = server.send_request(req)?;
        let response: GotoDefinitionResponse = server.get_response(request_id)?;
        match response {
            GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
            response => Err(anyhow::anyhow!(
                "Expected empty definitions, got `{:?}`",
                response
            )),
        }
    }

    #[test]
    fn sends_empty_goto_definition_on_non_access_symbol() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let uri = temp_file_uri("file.star");

        let mut server = TestServer::new()?;
        let contents = "y = 1\ndef nothing():\n    pass\nprint(nothing())\n";
        server.open_file(uri.clone(), contents.to_owned())?;

        let goto_definition = goto_definition_request(&mut server, uri, 1, 6);

        let request_id = server.send_request(goto_definition)?;
        let response = server.get_response::<GotoDefinitionResponse>(request_id)?;
        match response {
            GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
            response => Err(anyhow::anyhow!(
                "Expected empty definitions, got `{:?}`",
                response
            )),
        }
    }

    #[test]
    fn goes_to_definition() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let uri = temp_file_uri("file.star");
        let expected_location = expected_location_link(uri.clone(), 3, 6, 13, 1, 4, 11);

        let mut server = TestServer::new()?;
        let contents = "y = 1\ndef nothing():\n    pass\nprint(nothing())\n";
        server.open_file(uri.clone(), contents.to_owned())?;

        let goto_definition = goto_definition_request(&mut server, uri, 3, 6);

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn returns_old_definitions_if_current_file_does_not_parse() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let uri = temp_file_uri("file.star");
        let expected_location = expected_location_link(uri.clone(), 3, 6, 13, 1, 4, 11);

        let mut server = TestServer::new()?;
        let contents = "y = 1\ndef nothing():\n    pass\nprint(nothing())\n";
        server.open_file(uri.clone(), contents.to_owned())?;
        server.change_file(uri.clone(), "\"invalid parse".to_owned())?;

        let goto_definition = goto_definition_request(&mut server, uri, 3, 6);

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn jumps_to_definition_from_opened_loaded_file() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("{load}", "baz")
            <baz_click><baz>b</baz>az</baz_click>()
            "#,
        )
        .replace("{load}", bar_uri.path())
        .trim()
        .to_owned();
        let bar_contents = "def <baz>baz</baz>():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            bar_uri.clone(),
            foo.span("baz_click"),
            bar.span("baz"),
        );

        let mut server = TestServer::new()?;
        // Initialize with "junk" on disk so that we make sure we're using the contents from the
        // client (potentially indicating an unsaved, modified file)
        server.set_file_contents(PathBuf::from(bar_uri.path()), "some_symbol = 1".to_owned())?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.open_file(bar_uri, bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("baz"),
            foo.begin_column("baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn jumps_to_definition_from_closed_loaded_file() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("{load}", "baz")
            <baz_click><baz>b</baz>az</baz_click>()
            "#,
        )
        .replace("{load}", bar_uri.path())
        .trim()
        .to_owned();
        let bar_contents = "def <baz>baz</baz>():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            bar_uri.clone(),
            foo.span("baz_click"),
            bar.span("baz"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("baz"),
            foo.begin_column("baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn passes_cwd_for_relative_loads() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("bar.star", "baz")
            <baz_click><baz>b</baz>az</baz_click>()
            "#,
        )
        .trim()
        .to_owned();
        let bar_contents = "def <baz>baz</baz>():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            bar_uri.clone(),
            foo.span("baz_click"),
            bar.span("baz"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("baz"),
            foo.begin_column("baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn does_not_jump_to_definition_if_invalid_file() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");

        let foo_contents = dedent(
            r#"
            load("{load}", <baz_loc>"baz"</baz_loc>)
            <baz_click><baz>b</baz>az</baz_click>()
            "#,
        )
        .replace("{load}", foo_uri.path())
        .trim()
        .to_owned();
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let expected_location = expected_location_link_from_spans(
            foo_uri.clone(),
            foo.span("baz_click"),
            foo.span("baz_loc"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("baz"),
            foo.begin_column("baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn does_not_jump_to_definition_if_symbol_not_found() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("bar.star", <not_baz_loc>"not_baz"</not_baz_loc>)
            <not_baz>not_baz</not_baz>()
            "#,
        )
        .trim()
        .to_owned();
        let bar_contents = "def baz():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            foo_uri.clone(),
            foo.span("not_baz"),
            foo.span("not_baz_loc"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("not_baz"),
            foo.begin_column("not_baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn jumps_to_definition_in_load_statement() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("{load}", <baz_click>"<baz>b</baz>az"</baz_click>)
            baz()
            "#,
        )
        .replace("{load}", bar_uri.path())
        .trim()
        .to_owned();
        let bar_contents = "def <baz>baz</baz>():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            bar_uri.clone(),
            foo.span("baz_click"),
            bar.span("baz"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("baz"),
            foo.begin_column("baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn does_not_jump_to_definition_in_load_statement_if_not_found() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("bar.star", <not_baz_loc>"no<not_baz>t</not_baz>_baz"</not_baz_loc>)
            not_baz()
            "#,
        )
        .trim()
        .to_owned();
        let bar_contents = "def baz():\n    pass";
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;

        let expected_location = expected_location_link_from_spans(
            foo_uri.clone(),
            foo.span("not_baz_loc"),
            foo.span("not_baz_loc"),
        );

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("not_baz"),
            foo.begin_column("not_baz"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);
        Ok(())
    }

    #[test]
    fn jumps_to_file_in_load_statement() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");
        let bar_load_string = Path::new(bar_uri.path())
            .parent()
            .unwrap()
            .join("<bar>b</bar>ar<dot>.</dot>star")
            .display()
            .to_string();

        let foo_contents = dedent(
            r#"
            load(<bar_click>"{load}"</bar_click>, "baz")
            baz()
            "#,
        )
        .replace("{load}", &bar_load_string)
        .trim()
        .to_owned();
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;

        let expected_location = LocationLink {
            origin_selection_range: Some(foo.span("bar_click").into()),
            target_uri: bar_uri,
            target_range: Default::default(),
            target_selection_range: Default::default(),
        };

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("bar"),
            foo.begin_column("bar"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("dot"),
            foo.begin_column("dot"),
        );

        let request_id = server.send_request(goto_definition)?;
        let location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_location, location);

        Ok(())
    }

    #[test]
    fn jumps_to_definitions_in_strings() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let bar_contents = r#""Just <bar>a string</bar>""#;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), bar_contents)?;
        let bar_range = bar.span("bar");
        let bar_range_str = format!(
            "{}:{}:{}:{}",
            bar_range.begin_line, bar_range.begin_column, bar_range.end_line, bar_range.end_column
        );

        let foo_contents = dedent(
            r#"
            <foo1_click>"ba<foo1>r</foo1>.star"</foo1_click>
            [
                <foo2_click>"ba<foo2>r</foo2>.star"</foo2_click>,
                "ignored"
            ]
            {
                <foo3_click>"ba<foo3>r</foo3>.star"</foo3_click>: "ignored",
                "ignored": <foo4_click>"ba<foo4>r</foo4>.star"</foo4_click>,
                "ignored_other": "ignored",
            }

            def f1(x = <foo5_click>"ba<foo5>r</foo5>.star"</foo5_click>):
                <foo6_click>"ba<foo6>r</foo6>.star"</foo6_click>
                [
                    <foo7_click>"ba<foo7>r</foo7>.star"</foo7_click>,
                    "ignored"
                ]
                {
                    <foo8_click>"ba<foo8>r</foo8>.star"</foo8_click>: "ignored",
                    "ignored": <foo9_click>"ba<foo9>r</foo9>.star"</foo9_click>,
                    "ignored_other": "ignored",
                }
                if x == <foo10_click>"ba<foo10>r</foo10>.star"</foo10_click>:
                    <foo11_click>"ba<foo11>r</foo11>.star"</foo11_click>
                    [
                        <foo12_click>"ba<foo12>r</foo12>.star"</foo12_click>,
                        "ignored"
                    ]
                    {
                        <foo13_click>"ba<foo13>r</foo13>.star"</foo13_click>: "ignored",
                        "ignored": <foo14_click>"ba<foo14>r</foo14>.star"</foo14_click>,
                        "ignored_other": "ignored",
                    }
                return <foo15_click>"ba<foo15>r</foo15>.star"</foo15_click>

            foo16 = <foo16_click>"ba<foo16>r</foo16>.star"</foo16_click>

            <baz1_click>"ba<baz1>r</baz1>.star--{bar_range}"</baz1_click>
            [
                <baz2_click>"ba<baz2>r</baz2>.star--{bar_range}"</baz2_click>,
                "ignored"
            ]
            {
                <baz3_click>"ba<baz3>r</baz3>.star--{bar_range}"</baz3_click>: "ignored",
                "ignored": <baz4_click>"ba<baz4>r</baz4>.star--{bar_range}"</baz4_click>,
                "ignored_other": "ignored",
            }

            def f2(x = <baz5_click>"ba<baz5>r</baz5>.star--{bar_range}"</baz5_click>):
                <baz6_click>"ba<baz6>r</baz6>.star--{bar_range}"</baz6_click>
                [
                    <baz7_click>"ba<baz7>r</baz7>.star--{bar_range}"</baz7_click>,
                    "ignored"
                ]
                {
                    <baz8_click>"ba<baz8>r</baz8>.star--{bar_range}"</baz8_click>: "ignored",
                    "ignored": <baz9_click>"ba<baz9>r</baz9>.star--{bar_range}"</baz9_click>,
                    "ignored_other": "ignored",
                }
                if x == <baz10_click>"ba<baz10>r</baz10>.star--{bar_range}"</baz10_click>:
                    <baz11_click>"ba<baz11>r</baz11>.star--{bar_range}"</baz11_click>
                    [
                        <baz12_click>"ba<baz12>r</baz12>.star--{bar_range}"</baz12_click>,
                        "ignored"
                    ]
                    {
                        <baz13_click>"ba<baz13>r</baz13>.star--{bar_range}"</baz13_click>: "ignored",
                        "ignored": <baz14_click>"ba<baz14>r</baz14>.star--{bar_range}"</baz14_click>,
                        "ignored_other": "ignored",
                    }
                return <baz15_click>"ba<baz15>r</baz15>.star--{bar_range}"</baz15_click>

            baz16 = <baz16_click>"ba<baz16>r</baz16>.star--{bar_range}"</baz16_click>
            "#,
        )
        .trim()
        .replace("{bar_range}", &bar_range_str);
        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.open_file(bar_uri.clone(), bar.program())?;

        let mut test = |name: &str, expect_range: bool| -> anyhow::Result<()> {
            let range = if expect_range {
                bar_range
            } else {
                Default::default()
            };
            let expected_location = expected_location_link_from_spans(
                bar_uri.clone(),
                foo.span(&format!("{}_click", name)),
                range,
            );

            let goto_definition = goto_definition_request(
                &mut server,
                foo_uri.clone(),
                foo.begin_line(name),
                foo.begin_column(name),
            );
            let request_id = server.send_request(goto_definition)?;
            let location = goto_definition_response_location(&mut server, request_id)?;

            assert_eq!(expected_location, location);
            Ok(())
        };

        test("foo1", false)?;
        test("foo2", false)?;
        test("foo3", false)?;
        test("foo4", false)?;
        test("foo5", false)?;
        test("foo6", false)?;
        test("foo7", false)?;
        test("foo8", false)?;
        test("foo9", false)?;
        test("foo10", false)?;
        test("foo11", false)?;
        test("foo12", false)?;
        test("foo13", false)?;
        test("foo14", false)?;
        test("foo15", false)?;
        test("foo16", false)?;

        test("baz1", true)?;
        test("baz2", true)?;
        test("baz3", true)?;
        test("baz4", true)?;
        test("baz5", true)?;
        test("baz6", true)?;
        test("baz7", true)?;
        test("baz8", true)?;
        test("baz9", true)?;
        test("baz10", true)?;
        test("baz11", true)?;
        test("baz12", true)?;
        test("baz13", true)?;
        test("baz14", true)?;
        test("baz15", true)?;
        test("baz16", true)?;

        Ok(())
    }

    #[test]
    fn handles_paths_that_are_not_starlark() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");
        let baz_uri = temp_file_uri("baz");
        let dir1_uri = temp_file_uri("dir1");
        let dir2_uri = temp_file_uri("dir2.star");

        let foo_contents = dedent(
            r#"
            <bar>"b<bar_click>a</bar_click>r.star"</bar>
            <baz>"b<baz_click>a</baz_click>z"</baz>
            <dir1>"d<dir1_click>i</dir1_click>r1"</dir1>
            <dir2>"d<dir2_click>i</dir2_click>r2.star"</dir2>
            "#,
        )
        .trim()
        .to_owned();

        let bar_contents = dedent(
            r#"
            def ba r():
                # This has broken syntax
                pass
            "#,
        )
        .trim()
        .to_owned();

        let foo = FixtureWithRanges::from_fixture("foo.star", &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture("bar.star", &bar_contents)?;

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.set_file_contents(PathBuf::from(bar_uri.path()), bar.program())?;
        server.mkdir(dir1_uri.clone());
        server.mkdir(dir2_uri.clone());

        // File with broken syntax
        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("bar_click"),
            foo.begin_column("bar_click"),
        );
        let req_id = server.send_request(goto_definition)?;
        let response = goto_definition_response_location(&mut server, req_id)?;

        let expected = LocationLink {
            origin_selection_range: Some(foo.span("bar").into()),
            target_uri: bar_uri,
            target_range: Default::default(),
            target_selection_range: Default::default(),
        };
        assert_eq!(expected, response);

        // File that is not starlark at all
        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("baz_click"),
            foo.begin_column("baz_click"),
        );
        let req_id = server.send_request(goto_definition)?;
        let response = goto_definition_response_location(&mut server, req_id)?;

        let expected = LocationLink {
            origin_selection_range: Some(foo.span("baz").into()),
            target_uri: baz_uri,
            target_range: Default::default(),
            target_selection_range: Default::default(),
        };
        assert_eq!(expected, response);

        // Directory that doesn't look like a starlark file.
        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("dir1_click"),
            foo.begin_column("dir1_click"),
        );
        let req_id = server.send_request(goto_definition)?;
        let response = goto_definition_response_location(&mut server, req_id)?;

        let expected = LocationLink {
            origin_selection_range: Some(foo.span("dir1").into()),
            target_uri: dir1_uri,
            target_range: Default::default(),
            target_selection_range: Default::default(),
        };
        assert_eq!(expected, response);

        // Directory that looks like a starlark file by name, but isn't
        // File that is not starlark at all
        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("dir2_click"),
            foo.begin_column("dir2_click"),
        );
        let req_id = server.send_request(goto_definition)?;
        let response = goto_definition_response_location(&mut server, req_id)?;

        let expected = LocationLink {
            origin_selection_range: Some(foo.span("dir2").into()),
            target_uri: dir2_uri,
            target_range: Default::default(),
            target_selection_range: Default::default(),
        };
        assert_eq!(expected, response);
        Ok(())
    }

    #[test]
    fn disables_goto_definition() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let server = TestServer::new_with_settings(Some(LspServerSettings {
            enable_goto_definition: false,
        }))?;

        let goto_definition_disabled = server
            .initialization_result()
            .unwrap()
            .capabilities
            .definition_provider
            .is_none();

        assert!(goto_definition_disabled);

        let server = TestServer::new_with_settings(Some(LspServerSettings {
            enable_goto_definition: true,
        }))?;

        let goto_definition_enabled = server
            .initialization_result()
            .unwrap()
            .capabilities
            .definition_provider
            .is_some();

        assert!(goto_definition_enabled);
        Ok(())
    }

    #[test]
    fn returns_starlark_file_contents() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let mut server = TestServer::new()?;

        let uri = LspUrl::try_from(Url::parse("starlark:/native/builtin.bzl")?)?;
        let req = server.new_request::<StarlarkFileContentsRequest>(StarlarkFileContentsParams {
            uri: uri.clone(),
        });
        let request_id = server.send_request(req)?;
        let response = server.get_response::<StarlarkFileContentsResponse>(request_id)?;
        assert_eq!(
            server.docs_as_code(&uri).unwrap(),
            response.contents.unwrap()
        );

        let req = server.new_request::<StarlarkFileContentsRequest>(StarlarkFileContentsParams {
            uri: LspUrl::try_from(Url::parse("starlark:/native/not_builtin.bzl")?)?,
        });
        let request_id = server.send_request(req)?;
        let response = server.get_response::<StarlarkFileContentsResponse>(request_id)?;
        assert!(response.contents.is_none());

        Ok(())
    }

    #[test]
    fn goto_works_for_native_symbols() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let native_uri = Url::parse("starlark:/native/builtin.bzl")?;

        let mut server = TestServer::new()?;

        let foo_contents = dedent(
            r#"
            <click_n1>na<n1>t</n1>ive_function1</click_n1>()
            def f(<n2_loc>native_function1</n2_loc>):
                print(<click_n2>nat<n2>i</n2>ve_function1</click_n2>)
            mi<n3>s</n3>sing_global()
            "#,
        )
        .trim()
        .to_owned();
        let native_contents = dedent(
            r#"
            def <n1_loc>native_function1</n1_loc>():
                pass

            def native_function2():
                pass
            "#,
        )
        .trim()
        .to_owned();

        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let native = FixtureWithRanges::from_fixture(native_uri.path(), &native_contents)?;

        server.open_file(foo_uri.clone(), foo.program())?;

        let expected_n1_location = expected_location_link_from_spans(
            native_uri,
            foo.span("click_n1"),
            native.span("n1_loc"),
        );

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("n1"),
            foo.begin_column("n1"),
        );
        let request_id = server.send_request(goto_definition)?;
        let n1_location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_n1_location, n1_location);

        let expected_n2_location = expected_location_link_from_spans(
            foo_uri.clone(),
            foo.span("click_n2"),
            foo.span("n2_loc"),
        );

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri.clone(),
            foo.begin_line("n2"),
            foo.begin_column("n2"),
        );
        let request_id = server.send_request(goto_definition)?;
        let n2_location = goto_definition_response_location(&mut server, request_id)?;

        assert_eq!(expected_n2_location, n2_location);

        let goto_definition = goto_definition_request(
            &mut server,
            foo_uri,
            foo.begin_line("n3"),
            foo.begin_column("n3"),
        );
        let request_id = server.send_request(goto_definition)?;
        let n3_response = server.get_response::<GotoDefinitionResponse>(request_id)?;
        match n3_response {
            GotoDefinitionResponse::Array(definitions) if definitions.is_empty() => Ok(()),
            response => Err(anyhow::anyhow!(
                "Expected empty definitions, got `{:?}`",
                response
            )),
        }?;

        Ok(())
    }

    #[test]
    fn jumps_to_original_member_definition() -> anyhow::Result<()> {
        if is_wasm() {
            return Ok(());
        }

        let foo_uri = temp_file_uri("foo.star");
        let bar_uri = temp_file_uri("bar.star");

        let foo_contents = dedent(
            r#"
            load("{load}", "loaded")

            def <dest_baz>_baz</dest_baz>():
                pass

            <dest_quz>_quz</dest_quz> = 6

            <dest_root><dest_foobar>FooBarModule</dest_foobar></dest_root> = 5
            FooModule = struct(<dest_foo>foo</dest_foo> = 5)
            # Member value does not exist
            BarModule = struct(<dest_bar>bar</dest_bar> = bar)
            BazModule = struct(
                bar = bar,
                baz = _baz,
            )
            QuzModule = struct(bar = bar, baz = _baz, quz = _quz)

            <root>Foo<root_click>Bar</root_click>Module</root>.<foobar>f<foobar_click>o</foobar_click>obar</foobar>
            FooModule.<foo>f<foo_click>o</foo_click>o</foo>
            BarModule.<bar>b<bar_click>a</bar_click>r</bar>
            BazModule.<baz>b<baz_click>a</baz_click>z</baz>
            QuzModule.<quz>q<quz_click>u</quz_click>z</quz>
            loaded.<x><x_click>x</x_click></x>
            loaded.<y><y_click>y</y_click></y>
            "#,
        )
        .replace("{load}", bar_uri.path())
        .trim()
        .to_owned();

        let bar_contents = dedent(
            r#"
            def <dest_x>_x</dest_x>():
                pass
            <dest_y>loaded</dest_y> = struct(x = _x)
            "#,
        )
        .trim()
        .to_owned();

        let foo = FixtureWithRanges::from_fixture(foo_uri.path(), &foo_contents)?;
        let bar = FixtureWithRanges::from_fixture(bar_uri.path(), &bar_contents)?;

        let mut server = TestServer::new()?;
        server.open_file(foo_uri.clone(), foo.program())?;
        server.open_file(bar_uri.clone(), bar.program())?;

        let cases = [
            (&foo, &foo_uri, "root"),
            (&foo, &foo_uri, "foobar"),
            (&foo, &foo_uri, "foo"),
            (&foo, &foo_uri, "bar"),
            (&foo, &foo_uri, "baz"),
            (&foo, &foo_uri, "quz"),
            (&bar, &bar_uri, "x"),
            (&bar, &bar_uri, "y"),
        ];

        let expected_results = cases
            .iter()
            .map(|(fixture, uri, id)| {
                expected_location_link_from_spans(
                    (*uri).clone(),
                    foo.span(id),
                    fixture.span(&format!("dest_{}", id)),
                )
            })
            .collect::<Vec<_>>();

        let requests = cases
            .iter()
            .map(|(_, _, id)| {
                goto_definition_request(
                    &mut server,
                    foo_uri.clone(),
                    foo.begin_line(&format!("{}_click", id)),
                    foo.begin_column(&format!("{}_click", id)),
                )
            })
            .collect::<Vec<_>>();

        for (case, request, expected) in itertools::izip!(cases, requests, expected_results) {
            let req_id = server.send_request(request)?;
            let response = goto_definition_response_location(&mut server, req_id)
                .context(format!("getting response for case `{}`", case.2))?;

            assert_eq!(
                expected, response,
                "Incorrect response for case `{}`",
                case.2
            );
        }
        Ok(())
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Find which symbols are in scope at a particular point.

use starlark_map::small_map::SmallMap;

use crate::analysis::exported::SymbolKind;
use crate::codemap::CodeMap;
use crate::codemap::LineCol;
use crate::syntax::ast::AstAssignIdent;
use crate::syntax::ast::AstStmt;
use crate::syntax::ast::StmtP;
use crate::syntax::AstModule;

#[derive(Debug, PartialEq)]
pub(crate) struct SymbolWithDetails<'a> {
    /// The symbol itself.
    pub(crate) name: &'a str,
    /// The type of symbol it represents.
    pub(crate) kind: SymbolKind,
    /// The file where the symbol was loaded, if it was loaded.
    pub(crate) loaded_from: Option<&'a str>,
}

/// Find the symbols that are available in scope at a particular point that might be interesting
/// for autocomplete.
///
/// * Currently does not look into variables bound in list/dict comprehensions (should be fixed one day).
/// * Does not return local variables that start with an underscore (since they )
#[allow(dead_code)] // Used shortly by the LSP
pub(crate) fn find_symbols_at_position<'a>(
    module: &'a AstModule,
    position: LineCol,
) -> Vec<SymbolWithDetails<'a>> {
    fn walk<'a>(
        codemap: &CodeMap,
        position: LineCol,
        stmt: &'a AstStmt,
        top_level: bool,
        symbols: &mut SmallMap<&'a str, SymbolWithDetails<'a>>,
    ) {
        fn add<'a>(
            symbols: &mut SmallMap<&'a str, SymbolWithDetails<'a>>,
            top_level: bool,
            name: &'a AstAssignIdent,
            kind: SymbolKind,
            loaded_from: Option<&'a str>,
        ) {
            // Local variables which start with an underscore are ignored by convention,
            // so don't consider them to be variables we are interested in reporting.
            // They are more sinks to ignore warnings than variables.
            if top_level || !name.0.starts_with('_') {
                symbols.entry(&name.0).or_insert(SymbolWithDetails {
                    name: &name.0,
                    kind,
                    loaded_from,
                });
            }
        }

        match &**stmt {
            StmtP::Assign(dest, rhs) => dest
                .visit_lvalue(|x| add(symbols, top_level, x, SymbolKind::from_expr(&rhs.1), None)),
            StmtP::AssignModify(dest, _, _) => {
                dest.visit_lvalue(|x| add(symbols, top_level, x, SymbolKind::Any, None))
            }
            StmtP::For(dest, over_body) => {
                let (_, body) = &**over_body;
                dest.visit_lvalue(|x| add(symbols, top_level, x, SymbolKind::Any, None));
                walk(codemap, position, body, top_level, symbols);
            }
            StmtP::Def(def) => {
                add(symbols, top_level, &def.name, SymbolKind::Function, None);

                // Only recurse into method if the cursor is in it.
                if codemap.resolve_span(def.body.span).contains(position) {
                    for param in &def.params {
                        if let Some(name) = param.split().0 {
                            add(symbols, false, name, SymbolKind::Any, None)
                        }
                    }
                    walk(codemap, position, &def.body, false, symbols);
                }
            }
            StmtP::Load(load) => {
                for (name, _) in &load.args {
                    add(
                        symbols,
                        top_level,
                        name,
                        SymbolKind::Any,
                        Some(&**load.module),
                    )
                }
            }
            stmt => stmt.visit_stmt(|x| walk(codemap, position, x, top_level, symbols)),
        }
    }

    let mut symbols = SmallMap::new();
    walk(
        &module.codemap,
        position,
        &module.statement,
        true,
        &mut symbols,
    );
    symbols.into_values().collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::syntax::Dialect;

    #[test]
    fn test_symbols_at_location() {
        let modu = AstModule::parse(
            "t.star",
            r#"
load("foo.star", "exported_a", renamed = "exported_b")
def _method(param):
    pass
    _local = 7
    x = lambda _: 7
my_var = True
        "#
            .to_owned(),
            &Dialect::Standard,
        )
        .unwrap();

        let at_root = find_symbols_at_position(&modu, LineCol { line: 0, column: 0 });
        let mut inside_method = find_symbols_at_position(&modu, LineCol { line: 3, column: 6 });
        assert_eq!(inside_method.len(), 6);
        inside_method.retain(|x| !at_root.contains(x));

        let sym = |name, kind, loaded_from| SymbolWithDetails {
            name,
            kind,
            loaded_from,
        };

        assert_eq!(
            at_root,
            vec![
                sym("exported_a", SymbolKind::Any, Some("foo.star")),
                sym("renamed", SymbolKind::Any, Some("foo.star")),
                sym("_method", SymbolKind::Function, None),
                sym("my_var", SymbolKind::Any, None),
            ]
        );

        assert_eq!(
            inside_method,
            vec![
                sym("param", SymbolKind::Any, None),
                sym("x", SymbolKind::Function, None),
            ]
        );
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;

use dupe::Dupe;
use lsp_server::Connection;
use lsp_server::Message;
use lsp_server::RequestId;
use lsp_server::Response;
use lsp_server::ResponseError;
use lsp_types::notification::DidChangeTextDocument;
use lsp_types::notification::DidOpenTextDocument;
use lsp_types::notification::Exit;
use lsp_types::notification::Initialized;
use lsp_types::notification::Notification;
use lsp_types::notification::PublishDiagnostics;
use lsp_types::request::Initialize;
use lsp_types::request::Request;
use lsp_types::request::Shutdown;
use lsp_types::ClientCapabilities;
use lsp_types::DidChangeTextDocumentParams;
use lsp_types::DidOpenTextDocumentParams;
use lsp_types::GotoCapability;
use lsp_types::InitializeParams;
use lsp_types::InitializeResult;
use lsp_types::InitializedParams;
use lsp_types::Position;
use lsp_types::Range;
use lsp_types::TextDocumentClientCapabilities;
use lsp_types::TextDocumentContentChangeEvent;
use lsp_types::TextDocumentItem;
use lsp_types::Url;
use lsp_types::VersionedTextDocumentIdentifier;
use maplit::hashmap;
use serde::de::DeserializeOwned;

use crate::docs::render_docs_as_code;
use crate::docs::Doc;
use crate::docs::DocFunction;
use crate::docs::DocItem;
use crate::docs::Identifier;
use crate::docs::Location;
use crate::errors::EvalMessage;
use crate::lsp::server::new_notification;
use crate::lsp::server::server_with_connection;
use crate::lsp::server::LspContext;
use crate::lsp::server::LspEvalResult;
use crate::lsp::server::LspServerSettings;
use crate::lsp::server::LspUrl;
use crate::lsp::server::StringLiteralResult;
use crate::slice_vec_ext::VecExt;
use crate::syntax::AstModule;
use crate::syntax::Dialect;

/// Get the path from a URL, trimming off things like the leading slash that gets
/// appended in some windows test environments.
#[cfg(windows)]
fn get_path_from_uri(uri: &str) -> PathBuf {
    PathBuf::from(uri.trim_start_match('/'))
}

#[cfg(not(windows))]
fn get_path_from_uri(uri: &str) -> PathBuf {
    PathBuf::from(uri)
}

#[derive(thiserror::Error, Debug)]
enum ResolveLoadError {
    #[error("Relative path `{}` provided, but current_file_path could not be determined", .0.display())]
    MissingCurrentFilePath(PathBuf),
    #[error("Url `{}` was expected to be of type `{}`", .1, .0)]
    WrongScheme(String, LspUrl),
}

#[derive(thiserror::Error, Debug)]
pub(crate) enum TestServerError {
    #[error("Attempted to set the contents of a file with a non-absolute path `{}`", .0.display())]
    SetFileNotAbsolute(PathBuf),
    /// The response came back, but was an error response, not a successful one.
    #[error("Response error: {0:?}")]
    ResponseError(ResponseError),
    #[error("Invalid response message for request {0}: {1:?}")]
    InvalidResponse(RequestId, Response),
    #[error("Client received a request (not response/notification) from the server: {0:?}")]
    ReceivedRequest(lsp_server::Request),
    #[error("Got a duplicate response for request ID {:?}: Existing: {:?}, New: {:?}", .new.id, .existing, .new)]
    DuplicateResponse { new: Response, existing: Response },
    /// The provided Url was not absolute and it needs to be.
    #[error("Path for URL `{}` was not absolute", .0)]
    NotAbsolute(LspUrl),
    #[error("Path is directory: `{0}`")]
    IsADirectory(LspUrl),
}

struct TestServerContext {
    file_contents: Arc<RwLock<HashMap<PathBuf, String>>>,
    dirs: Arc<RwLock<HashSet<PathBuf>>>,
    builtin_docs: Arc<HashMap<LspUrl, String>>,
    builtin_symbols: Arc<HashMap<String, LspUrl>>,
}

impl LspContext for TestServerContext {
    fn parse_file_with_contents(&self, uri: &LspUrl, content: String) -> LspEvalResult {
        match uri {
            LspUrl::File(path) | LspUrl::Starlark(path) => {
                match AstModule::parse(&path.to_string_lossy(), content, &Dialect::Extended) {
                    Ok(ast) => {
                        let diagnostics = ast.lint(None).into_map(|l| EvalMessage::from(l).into());
                        LspEvalResult {
                            diagnostics,
                            ast: Some(ast),
                        }
                    }
                    Err(e) => {
                        let diagnostics = vec![EvalMessage::from_anyhow(path, &e).into()];
                        LspEvalResult {
                            diagnostics,
                            ast: None,
                        }
                    }
                }
            }
            _ => LspEvalResult::default(),
        }
    }

    fn resolve_load(&self, path: &str, current_file: &LspUrl) -> anyhow::Result<LspUrl> {
        let path = PathBuf::from(path);
        match current_file {
            LspUrl::File(current_file_path) => {
                let current_file_dir = current_file_path.parent();
                let absolute_path = match (current_file_dir, path.is_absolute()) {
                    (_, true) => Ok(path),
                    (Some(current_file_dir), false) => Ok(current_file_dir.join(&path)),
                    (None, false) => Err(ResolveLoadError::MissingCurrentFilePath(path)),
                }?;
                Ok(Url::from_file_path(absolute_path).unwrap().try_into()?)
            }
            _ => Err(
                ResolveLoadError::WrongScheme("file://".to_owned(), current_file.clone()).into(),
            ),
        }
    }

    fn resolve_string_literal(
        &self,
        literal: &str,
        current_file: &LspUrl,
    ) -> anyhow::Result<Option<StringLiteralResult>> {
        let re = regex::Regex::new(r#"--(\d+):(\d+):(\d+):(\d+)$"#)?;
        let (literal, range) = match re.captures(literal) {
            Some(cap) => {
                let start_line = cap.get(1).unwrap().as_str().parse().unwrap();
                let start_col = cap.get(2).unwrap().as_str().parse().unwrap();
                let end_line = cap.get(3).unwrap().as_str().parse().unwrap();
                let end_col = cap.get(4).unwrap().as_str().parse().unwrap();
                let range = Range::new(
                    Position::new(start_line, start_col),
                    Position::new(end_line, end_col),
                );
                (
                    literal[0..cap.get(0).unwrap().start()].to_owned(),
                    Some(range),
                )
            }
            None => (literal.to_owned(), None),
        };
        self.resolve_load(&literal, current_file)
            .map(|url| match &url {
                LspUrl::File(u) => match u.extension() {
                    Some(e) if e == "star" => Some(StringLiteralResult {
                        url,
                        location_finder: Some(Box::new(move |_ast, _url| Ok(range))),
                    }),
                    _ => Some(StringLiteralResult {
                        url,
                        location_finder: None,
                    }),
                },
                _ => None,
            })
    }

    fn get_load_contents(&self, uri: &LspUrl) -> anyhow::Result<Option<String>> {
        match uri {
            LspUrl::File(u) => {
                let path = get_path_from_uri(&u.to_string_lossy());
                let is_dir = self.dirs.read().unwrap().contains(&path);
                match (path.is_absolute(), is_dir) {
                    (true, false) => Ok(self.file_contents.read().unwrap().get(&path).cloned()),
                    (true, true) => Err(TestServerError::IsADirectory(uri.clone()).into()),
                    (false, _) => Err(TestServerError::NotAbsolute(uri.clone()).into()),
                }
            }
            LspUrl::Starlark(_) => Ok(self.builtin_docs.get(uri).cloned()),
            _ => Ok(None),
        }
    }

    fn get_url_for_global_symbol(
        &self,
        _current_file: &LspUrl,
        symbol: &str,
    ) -> anyhow::Result<Option<LspUrl>> {
        Ok(self.builtin_symbols.get(symbol).cloned())
    }
}

/// A server for use in testing that provides helpers for sending requests, correlating
/// responses, and sending / receiving notifications
pub struct TestServer {
    /// The thread that's actually running the server
    server_thread: Option<std::thread::JoinHandle<()>>,
    client_connection: Connection,
    /// Incrementing counter to automatically generate request IDs when making a request
    req_counter: i32,
    /// Simple incrementing document version counter
    version_counter: i32,
    /// A mapping of the requests that have arrived -> the response. Stored here as
    /// these responses might be interleaved with notifications and the like.
    responses: HashMap<RequestId, Response>,
    /// An ordered queue of all of the notifications that have been received. Drained as
    /// notifications are processed.
    notifications: VecDeque<lsp_server::Notification>,
    /// How long to wait for messages to be received.
    recv_timeout: Duration,
    file_contents: Arc<RwLock<HashMap<PathBuf, String>>>,
    dirs: Arc<RwLock<HashSet<PathBuf>>>,
    /// If it's been received, the response payload for initialization.
    initialize_response: Option<InitializeResult>,
    /// Documentation for built in symbols.
    builtin_docs: Arc<HashMap<LspUrl, String>>,
}

impl Drop for TestServer {
    fn drop(&mut self) {
        // Need to send both a Shutdown request and an Exit notification in succession
        // so that lsp_server knows to shutdown correctly.
        let req = lsp_server::Request {
            id: self.next_request_id(),
            method: Shutdown::METHOD.to_owned(),
            params: Default::default(),
        };
        if let Err(e) = self.send_request(req) {
            eprintln!("Server was already shutdown: {}", e);
        } else {
            let notif = lsp_server::Notification {
                method: Exit::METHOD.to_owned(),
                params: Default::default(),
            };
            if let Err(e) = self.send_notification(notif) {
                eprintln!("Could not send Exit notification: {}", e);
            }
        }

        if let Some(server_thread) = self.server_thread.take() {
            if let Err(e) = server_thread.join() {
                eprintln!("test server did not join when being dropped: {:?}", e);
            }
        }
    }
}

impl TestServer {
    pub(crate) fn docs_as_code(&self, uri: &LspUrl) -> Option<String> {
        self.builtin_docs.get(uri).cloned()
    }

    /// A static set of "builtins" to use for testing
    fn testing_builtins(root: &Path) -> anyhow::Result<HashMap<LspUrl, Vec<Doc>>> {
        let prelude_path = root.join("dir/prelude.bzl");
        let ret = hashmap! {
            LspUrl::try_from(Url::parse("starlark:/native/builtin.bzl")?)? => vec![
                Doc::named_item("native_function1".to_owned(), DocItem::Function(DocFunction::default())),
                Doc::named_item("native_function2".to_owned(),DocItem::Function(DocFunction::default())),
            ],
            LspUrl::try_from(Url::from_file_path(prelude_path).unwrap())? => vec![
                Doc {
                    id: Identifier {
                        name: "prelude_function".to_owned(),
                        location: Some(Location {
                            path: "//dir/prelude.bzl".to_owned(),
                            position: None,
                        }),
                    },
                    item: DocItem::Function(DocFunction::default()),
                    custom_attrs: Default::default(),
                },
            ]
        };
        Ok(ret)
    }

    /// Generate a new request ID
    fn next_request_id(&mut self) -> RequestId {
        self.req_counter += 1;
        RequestId::from(self.req_counter)
    }

    fn next_document_version(&mut self) -> i32 {
        self.version_counter += 1;
        self.version_counter
    }

    /// Create a new request object with an automatically generated request ID.
    pub fn new_request<T: Request>(&mut self, params: T::Params) -> lsp_server::Request {
        lsp_server::Request {
            id: self.next_request_id(),
            method: T::METHOD.to_owned(),
            params: serde_json::to_value(params).unwrap(),
        }
    }

    /// Create and start a new LSP server. This sends the initialization messages with the given,
    /// initialization payload and makes sure that when the server is dropped, the threads
    /// are attempted to be stopped.
    pub(crate) fn new_with_settings(settings: Option<LspServerSettings>) -> anyhow::Result<Self> {
        let (server_connection, client_connection) = Connection::memory();

        let builtin = Self::testing_builtins(&std::env::current_dir()?)?;
        let mut builtin_docs = HashMap::with_capacity(builtin.len());
        let mut builtin_symbols = HashMap::new();

        for (u, ds) in builtin {
            builtin_docs.insert(u.clone(), render_docs_as_code(&ds));
            for d in ds {
                builtin_symbols.insert(d.id.name, u.clone());
            }
        }

        let builtin_docs = Arc::new(builtin_docs);
        let builtin_symbols = Arc::new(builtin_symbols);

        let prelude_file_contents = builtin_docs
            .iter()
            .filter_map(|(u, d)| match u {
                LspUrl::File(p) => Some((p.clone(), d.clone())),
                _ => None,
            })
            .collect();
        let file_contents = Arc::new(RwLock::new(prelude_file_contents));
        let dirs = Arc::new(RwLock::new(HashSet::new()));
        let ctx = TestServerContext {
            file_contents: file_contents.dupe(),
            dirs: dirs.dupe(),
            builtin_docs: builtin_docs.dupe(),
            builtin_symbols,
        };

        let server_thread = std::thread::spawn(|| {
            if let Err(e) = server_with_connection(server_connection, ctx) {
                eprintln!("Stopped test server thread with error `{:?}`", e);
            }
        });

        let ret = Self {
            server_thread: Some(server_thread),
            client_connection,
            req_counter: 0,
            version_counter: 0,
            responses: Default::default(),
            notifications: Default::default(),
            recv_timeout: Duration::from_secs(2),
            file_contents,
            dirs,
            initialize_response: None,
            builtin_docs,
        };
        ret.initialize(settings)
    }

    /// Create and start a new LSP server. This sends the initialization messages, and makes
    /// sure that when the server is dropped, the threads are attempted to be stopped.
    pub(crate) fn new() -> anyhow::Result<Self> {
        Self::new_with_settings(None)
    }

    fn initialize(mut self, settings: Option<LspServerSettings>) -> anyhow::Result<Self> {
        let capabilities = ClientCapabilities {
            text_document: Some(TextDocumentClientCapabilities {
                definition: Some(GotoCapability {
                    dynamic_registration: Some(true),
                    link_support: Some(true),
                }),
                ..Default::default()
            }),
            ..Default::default()
        };

        let initialization_options = settings.map(|v| serde_json::to_value(v).unwrap());

        #[allow(deprecated)]
        let init = InitializeParams {
            process_id: None,
            root_path: None,
            root_uri: None,
            initialization_options,
            capabilities,
            trace: None,
            workspace_folders: None,
            client_info: None,
            locale: None,
        };

        let init_request = self.new_request::<Initialize>(init);
        let initialize_id = self.send_request(init_request)?;

        let initialize_response = Some(self.get_response::<InitializeResult>(initialize_id)?);

        self.send_notification(lsp_server::Notification {
            method: Initialized::METHOD.to_owned(),
            params: serde_json::to_value(InitializedParams {})?,
        })?;

        self.initialize_response = initialize_response;

        Ok(self)
    }

    pub fn initialization_result(&self) -> Option<InitializeResult> {
        self.initialize_response.clone()
    }

    /// Send a request to the server, and get back the ID from the original message.
    pub fn send_request(&self, req: lsp_server::Request) -> anyhow::Result<RequestId> {
        let id = req.id.clone();
        self.send(Message::Request(req))?;
        Ok(id)
    }

    /// Send a notification to the server.
    pub fn send_notification(&self, notification: lsp_server::Notification) -> anyhow::Result<()> {
        self.send(Message::Notification(notification))
    }

    fn send(&self, message: Message) -> anyhow::Result<()> {
        Ok(self.client_connection.sender.send(message)?)
    }

    /// Receive messages from the server until either the response for the given request ID
    /// has been seen, or until there are no more messages and the receive method times out.
    pub fn get_response<T: DeserializeOwned>(&mut self, id: RequestId) -> anyhow::Result<T> {
        loop {
            self.receive()?;

            match self.responses.get(&id) {
                Some(Response {
                    error: None,
                    result: Some(result),
                    ..
                }) => {
                    break Ok(serde_json::from_value::<T>(result.clone())?);
                }
                Some(Response {
                    error: Some(err),
                    result: None,
                    ..
                }) => {
                    break Err(TestServerError::ResponseError(err.clone()).into());
                }
                Some(msg) => {
                    break Err(TestServerError::InvalidResponse(id, msg.clone()).into());
                }
                None => {}
            }
        }
    }

    pub fn get_notification<T: Notification>(&mut self) -> anyhow::Result<T::Params> {
        for _ in 0..10 {
            self.receive()?;
            let notification = self
                .notifications
                .iter()
                .enumerate()
                .find_map(|(i, n)| if T::METHOD == n.method { Some(i) } else { None })
                .and_then(|i| self.notifications.remove(i));
            if let Some(notification) = notification {
                return Ok(serde_json::from_value(notification.params)?);
            }
        }
        Err(anyhow::anyhow!(
            "Did not get a notification of type `{}` in 10 retries",
            T::METHOD
        ))
    }

    /// Attempt to receive a message and either put it in the `responses` map if it's a
    /// response, or the notifications queue if it's a notification.
    ///
    /// Returns an error if an invalid message is received, or if no message is
    /// received within the timeout.
    fn receive(&mut self) -> anyhow::Result<()> {
        let message = self
            .client_connection
            .receiver
            .recv_timeout(self.recv_timeout)?;
        match message {
            Message::Request(req) => Err(TestServerError::ReceivedRequest(req).into()),
            Message::Response(response) => match self.responses.entry(response.id.clone()) {
                Entry::Occupied(existing) => Err(TestServerError::DuplicateResponse {
                    new: response,
                    existing: existing.get().clone(),
                }
                .into()),
                Entry::Vacant(entry) => {
                    entry.insert(response);
                    Ok(())
                }
            },
            Message::Notification(notification) => {
                self.notifications.push_back(notification);
                Ok(())
            }
        }
    }

    /// Send a notification saying that a file was opened with the given contents.
    ///
    /// This will return an error if there were any diagnostic messages.
    pub fn open_file(&mut self, uri: Url, contents: String) -> anyhow::Result<()> {
        let open_params = DidOpenTextDocumentParams {
            text_document: TextDocumentItem {
                uri: uri.clone(),
                language_id: String::new(),
                version: self.next_document_version(),
                text: contents,
            },
        };
        let open_notification = new_notification::<DidOpenTextDocument>(open_params);
        self.send_notification(open_notification)?;
        let notification = self.get_notification::<PublishDiagnostics>()?;
        if notification.uri != uri {
            Err(anyhow::anyhow!(
                "Got a diagnostics message for `{}`, but expected it for `{}`",
                notification.uri,
                uri
            ))
        } else if !notification.diagnostics.is_empty() {
            Err(anyhow::anyhow!(
                "Got unexpected diagnostic messages when opening {}, got {:?}",
                uri,
                notification.diagnostics
            ))
        } else {
            Ok(())
        }
    }

    /// Send a notification saying that a file was changed with the given contents.
    pub fn change_file(&mut self, uri: Url, contents: String) -> anyhow::Result<()> {
        let change_params = DidChangeTextDocumentParams {
            text_document: VersionedTextDocumentIdentifier {
                uri,
                version: self.next_document_version(),
            },
            content_changes: vec![TextDocumentContentChangeEvent {
                range: None,
                range_length: None,
                text: contents,
            }],
        };
        let change_notification = new_notification::<DidChangeTextDocument>(change_params);
        self.send_notification(change_notification)?;
        Ok(())
    }

    /// Set the file contents that `get_load_contents()` will return. The path must be absolute.
    pub fn set_file_contents(&self, path: PathBuf, contents: String) -> anyhow::Result<()> {
        let path = get_path_from_uri(&format!("{}", path.display()));
        if !path.is_absolute() {
            Err(TestServerError::SetFileNotAbsolute(path).into())
        } else {
            self.file_contents.write().unwrap().insert(path, contents);
            Ok(())
        }
    }

    /// Configure a path to be "a directory". This will return IsADirectory as an
    /// error from get_load_contents
    pub fn mkdir(&self, uri: Url) {
        self.dirs.write().unwrap().insert(PathBuf::from(uri.path()));
    }
}