mq-lsp 0.5.14

Language Server Protocol implementation for mq query language
Documentation
use std::{
    str::FromStr,
    sync::{Arc, RwLock},
};

use tower_lsp_server::ls_types::{self, GotoDefinitionResponse, Location, Position, Range};
use url::Url;

pub fn response(hir: Arc<RwLock<mq_hir::Hir>>, url: Url, position: Position) -> Option<GotoDefinitionResponse> {
    let hir_guard = hir.read().unwrap();
    let source = hir_guard.source_by_url(&url);

    if let Some(source) = source {
        if let Some((_, symbol)) = hir_guard.find_symbol_in_position(
            source,
            mq_lang::Position::new(position.line + 1, (position.character + 1) as usize),
        ) {
            symbol.source.text_range.and_then(|text_range| {
                // Get the URL for the symbol's actual source file
                // Returns None for builtin symbols (no file to navigate to)
                let target_url = symbol
                    .source
                    .source_id
                    .and_then(|sid| hir_guard.url_by_source(&sid))
                    .cloned()?;

                let uri = match ls_types::Uri::from_str(target_url.as_ref()) {
                    Ok(uri) => uri,
                    Err(_) => return None,
                };

                Some(GotoDefinitionResponse::Scalar(Location {
                    uri,
                    range: Range {
                        start: Position {
                            line: text_range.start.line - 1,
                            character: (text_range.start.column - 1) as u32,
                        },
                        end: Position {
                            line: text_range.end.line - 1,
                            character: (text_range.end.column - 1) as u32,
                        },
                    },
                }))
            })
        } else {
            None
        }
    } else {
        None
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use mq_hir::Hir;

    #[test]
    fn test_goto_definition_found() {
        let mut hir = Hir::default();
        let url = Url::parse("file:///test.mq").unwrap();
        hir.add_code(Some(url.clone()), "def func1(): 1; func1()");

        let result = response(Arc::new(RwLock::new(hir)), url.clone(), Position::new(0, 4));

        assert!(result.is_some());
        if let Some(GotoDefinitionResponse::Scalar(location)) = result {
            assert_eq!(location.uri.to_string(), url.to_string());
            assert_eq!(location.range.start, Position::new(0, 4));
            assert_eq!(location.range.end, Position::new(0, 9));
        } else {
            panic!("Expected Scalar response");
        }
    }

    #[test]
    fn test_goto_definition_not_found() {
        let hir = Arc::new(RwLock::new(mq_hir::Hir::default()));
        let url = Url::parse("file:///test.mq").unwrap();

        let result = response(hir, url, Position::new(0, 0));
        assert!(result.is_none());
    }

    #[test]
    fn test_goto_definition_no_text_range() {
        let mut hir = mq_hir::Hir::default();
        let url = Url::parse("file:///test.mq").unwrap();
        hir.add_code(Some(url.clone()), "let x = 42;");

        let result = response(Arc::new(RwLock::new(hir)), url, Position::new(0, 11));
        assert!(result.is_none());
    }

    #[test]
    fn test_goto_definition_call_to_definition() {
        let mut hir = Hir::default();
        let url = Url::parse("file:///test.mq").unwrap();
        // Define function then call it
        hir.add_code(Some(url.clone()), "def helper(): 42 end | helper()");

        // Position on helper() call (at character 24, after "| ")
        let result = response(Arc::new(RwLock::new(hir)), url.clone(), Position::new(0, 24));

        // Should navigate to the function definition in the same file
        assert!(result.is_some());
        if let Some(GotoDefinitionResponse::Scalar(location)) = result {
            assert_eq!(location.uri.to_string(), url.to_string());
            // Definition starts at "def helper"
            assert_eq!(location.range.start, Position::new(0, 4));
        } else {
            panic!("Expected Scalar response");
        }
    }

    #[test]
    fn test_goto_definition_builtin_returns_none() {
        let mut hir = Hir::default();
        let url = Url::parse("file:///test.mq").unwrap();
        hir.add_code(Some(url.clone()), "\"test\" | len");

        // Position on "len" builtin function call (at character 10)
        let result = response(Arc::new(RwLock::new(hir)), url.clone(), Position::new(0, 10));

        // Builtins should return None (no file to navigate to)
        assert!(result.is_none());
    }
}