hurl-lsp 0.1.11

Language Server Protocol implementation for Hurl
use crate::syntax::variable_placeholders;
use std::collections::BTreeMap;
use tower_lsp::lsp_types::{Hover, HoverContents, MarkedString, Position};

const SECTION_DOCS: &[(&str, &str)] = &[
    ("[Headers]", "HTTP request headers."),
    ("[Asserts]", "Assertions evaluated against the response."),
    (
        "[Captures]",
        "Values extracted from the response for later reuse.",
    ),
    ("[Options]", "Request execution options."),
];

const ASSERT_DOCS: &[(&str, &str)] = &[
    (
        "jsonpath",
        "Evaluate a JSONPath expression against the response body.",
    ),
    (
        "xpath",
        "Evaluate an XPath expression against the response body.",
    ),
    ("regex", "Assert with a regular expression."),
    ("status", "Assert against the HTTP status code."),
    (
        "duration",
        "Assert against total request duration in milliseconds.",
    ),
];

const METHOD_DOCS: &[(&str, &str)] = &[
    ("GET", "Retrieve a representation of a resource."),
    ("POST", "Submit data to create or trigger a resource."),
    ("PUT", "Replace a resource with the provided payload."),
    ("PATCH", "Partially update a resource."),
    ("DELETE", "Delete a resource."),
];

#[cfg(test)]
pub fn hover(text: &str, position: Position) -> Option<Hover> {
    hover_with_external(text, position, &BTreeMap::new())
}

pub fn hover_with_external(
    text: &str,
    position: Position,
    external_variables: &BTreeMap<String, String>,
) -> Option<Hover> {
    let line = text.lines().nth(position.line as usize)?;
    let ch = position.character as usize;
    for (start, end, variable) in variable_placeholders(line) {
        if ch >= start && ch <= end {
            if let Some(value) = external_variables.get(variable) {
                return Some(Hover {
                    contents: HoverContents::Scalar(MarkedString::String(format!(
                        "**{}**\n\n`{}`",
                        variable, value
                    ))),
                    range: None,
                });
            }
        }
    }

    let token = token_at(line, position.character as usize)?;
    let docs = SECTION_DOCS
        .iter()
        .chain(ASSERT_DOCS.iter())
        .chain(METHOD_DOCS.iter())
        .find(|(label, _)| *label == token)?;

    Some(Hover {
        contents: HoverContents::Scalar(MarkedString::String(format!(
            "**{}**\n\n{}",
            docs.0, docs.1
        ))),
        range: None,
    })
}

fn token_at(line: &str, character: usize) -> Option<&str> {
    if line.trim_start().starts_with('[') {
        return Some(line.trim());
    }

    let idx = character.min(line.len());
    let bytes = line.as_bytes();
    let mut start = idx;
    while start > 0 && !bytes[start - 1].is_ascii_whitespace() {
        start -= 1;
    }
    let mut end = idx;
    while end < bytes.len() && !bytes[end].is_ascii_whitespace() {
        end += 1;
    }
    if start == end {
        None
    } else {
        line.get(start..end)
    }
}

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

    #[test]
    fn returns_hover_for_method() {
        let value = hover("GET https://example.com", Position::new(0, 1));
        assert!(value.is_some());
    }

    #[test]
    fn returns_hover_for_external_variable_value() {
        let mut vars = BTreeMap::new();
        vars.insert("host".to_string(), "example.com".to_string());
        let value = hover_with_external("GET https://{{host}}/users", Position::new(0, 16), &vars);
        assert!(value.is_some());
    }
}