lspf 1.0.3

A Rust framework for building extensible LSP language servers
Documentation
//! Shared utilities for the feature example servers.

#![allow(dead_code)]

// The transport examples reach the same module directly; they serve a socket
// instead of stdio, so they share the log format but not `serve` below.
#[path = "../example_logging/mod.rs"]
mod example_logging;

use lspf::types::{Diagnostic, DiagnosticSeverity, Position, Range, Uri};
use lspf::{Document, LspError, PositionEncoding, Server, ServerContext};

pub(crate) async fn serve<S: Send + Sync + 'static>(server: Server<S>) -> lspf::Result<()> {
    example_logging::init();
    let outcome = lspf::stdio(server).serve().await?;
    std::process::exit(outcome.code());
}

pub(crate) fn document(ctx: &ServerContext, uri: &Uri) -> Result<Document, LspError> {
    ctx.documents()
        .get(uri)
        .ok_or_else(|| LspError::invalid_params(format!("document is not open: {}", uri.as_str())))
}

pub(crate) fn text(ctx: &ServerContext, uri: &Uri) -> Result<String, LspError> {
    Ok(document(ctx, uri)?.text(None).into_owned())
}

pub(crate) fn line_range(line: u32, text: &str) -> Range {
    Range::new(
        Position::new(line, 0),
        Position::new(line, text.trim_end_matches(['\r', '\n']).len() as u32),
    )
}

pub(crate) fn parse_incomplete_sum(line: &str) -> Option<(u64, u64)> {
    let expression = line.trim().strip_suffix('=')?.trim();
    let (left, right) = expression.split_once('+')?;
    Some((left.trim().parse().ok()?, right.trim().parse().ok()?))
}

pub(crate) fn parse_sum(line: &str) -> Option<(u64, u64, Option<u64>)> {
    let (expression, answer) = line.trim().split_once('=')?;
    let (left, right) = expression.split_once('+')?;
    let answer = match answer.trim() {
        "" => None,
        value => Some(value.parse().ok()?),
    };
    Some((
        left.trim().parse().ok()?,
        right.trim().parse().ok()?,
        answer,
    ))
}

pub(crate) fn sum_diagnostics(text: &str) -> Vec<Diagnostic> {
    text.lines()
        .enumerate()
        .filter_map(|(line_number, line)| {
            let (left, right, answer) = parse_sum(line)?;
            if answer == Some(left + right) {
                return None;
            }
            let (severity, message) = match answer {
                None => (DiagnosticSeverity::Warning, "Missing answer".to_string()),
                Some(actual) => (
                    DiagnosticSeverity::Error,
                    format!("Incorrect answer: {actual}"),
                ),
            };
            Some(Diagnostic {
                range: line_range(line_number as u32, line),
                severity: Some(severity),
                source: Some("lspf-example".to_string()),
                message: lspf::types::Message::String(message),
                ..Diagnostic::default()
            })
        })
        .collect()
}

pub(crate) fn word_ranges(text: &str, needle: &str, encoding: PositionEncoding) -> Vec<Range> {
    text.lines()
        .enumerate()
        .flat_map(|(line, text)| {
            text.match_indices(needle).filter_map(move |(start, _)| {
                let end = start + needle.len();
                let boundary = |index: usize| {
                    text.as_bytes()
                        .get(index)
                        .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
                };
                (boundary(start.wrapping_sub(1)) && boundary(end)).then(|| {
                    Range::new(
                        Position::new(line as u32, column(&text[..start], encoding)),
                        Position::new(line as u32, column(&text[..end], encoding)),
                    )
                })
            })
        })
        .collect()
}

#[cfg(all(test, feature = "testing"))]
pub(crate) mod text_tests;

fn column(prefix: &str, encoding: PositionEncoding) -> u32 {
    match encoding {
        PositionEncoding::Utf8 => prefix.len() as u32,
        PositionEncoding::Utf16 => prefix.encode_utf16().count() as u32,
        PositionEncoding::Utf32 => prefix.chars().count() as u32,
    }
}