mcpls-core 0.8.0

Core library for MCP to LSP protocol translation
Documentation
//! What is known about whether a server answers `textDocument/diagnostic`.
//!
//! `diagnosticProvider` is the advertisement, but not the truth: pyright
//! advertises none and answers pulls anyway, and a push-only server answers
//! `-32601`. A server that advertises nothing is therefore probed once; what
//! the probe showed is kept per process in its slot.

/// What probing a server that advertises no pull provider has shown so far.
///
/// Held in the server's slot and fresh for every replacement process.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum PullProbe {
    /// No pull request has been sent to the process yet.
    #[default]
    Untried,
    /// The process answered a pull request.
    Answered,
    /// The process answered a pull request with `-32601`, or timed out on two
    /// in a row.
    Refused,
    /// A pull request timed out: a second one that was sent after that
    /// timeout, and times out too, refuses the process.
    ///
    /// One timeout is not enough, since a large workspace's first pull can
    /// outlast the request timeout on a server that answers pulls.
    TimedOutOnce,
}

impl PullProbe {
    /// The probe after a pull request that was sent while the probe was
    /// `sent_with` timed out.
    ///
    /// Only a request sent after an earlier timeout can be the second one: two
    /// requests in flight together that both time out are one failure to
    /// answer in time, not two. An answer ends the count, and a transient
    /// reply (`-32800`, `-32801`, `-32802`) neither adds to it nor ends it.
    pub(super) const fn after_timeout(self, sent_with: Self) -> Self {
        match self {
            Self::Answered | Self::Refused => self,
            Self::Untried | Self::TimedOutOnce => match sent_with {
                Self::TimedOutOnce | Self::Refused => Self::Refused,
                Self::Untried | Self::Answered => Self::TimedOutOnce,
            },
        }
    }
}

/// Whether `get_diagnostics` can expect a pull answer from a server.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PullSupport {
    /// The server advertises `diagnosticProvider`.
    Advertised,
    /// Nothing advertised and not tried yet: the next pull is the probe.
    Probing,
    /// Nothing advertised, but the server answered a pull.
    Answers,
    /// Nothing advertised, and the server refused a pull.
    Unsupported,
}

impl PullSupport {
    /// The support of a server that advertises a pull provider or not, with
    /// what its probe showed.
    pub(super) const fn of(advertised: bool, probe: PullProbe) -> Self {
        match (advertised, probe) {
            (true, _) => Self::Advertised,
            (false, PullProbe::Untried | PullProbe::TimedOutOnce) => Self::Probing,
            (false, PullProbe::Answered) => Self::Answers,
            (false, PullProbe::Refused) => Self::Unsupported,
        }
    }

    /// Whether `get_diagnostics` sends a pull request.
    pub(super) const fn sends_pull(self) -> bool {
        !matches!(self, Self::Unsupported)
    }
}

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

    #[test]
    fn test_advertising_wins_over_any_probe_result() {
        for probe in [
            PullProbe::Untried,
            PullProbe::TimedOutOnce,
            PullProbe::Answered,
            PullProbe::Refused,
        ] {
            assert_eq!(PullSupport::of(true, probe), PullSupport::Advertised);
        }
    }

    #[test]
    fn test_unadvertised_support_follows_the_probe() {
        assert_eq!(
            PullSupport::of(false, PullProbe::Untried),
            PullSupport::Probing
        );
        assert_eq!(
            PullSupport::of(false, PullProbe::Answered),
            PullSupport::Answers
        );
        assert_eq!(
            PullSupport::of(false, PullProbe::Refused),
            PullSupport::Unsupported
        );
    }

    #[test]
    fn test_one_timeout_keeps_probing_and_a_later_sent_second_refuses() {
        let once = PullProbe::Untried.after_timeout(PullProbe::Untried);
        assert_eq!(once, PullProbe::TimedOutOnce);
        assert_eq!(PullSupport::of(false, once), PullSupport::Probing);
        assert_eq!(once.after_timeout(once), PullProbe::Refused);
    }

    #[test]
    fn test_overlapping_timeouts_count_once() {
        let once = PullProbe::Untried.after_timeout(PullProbe::Untried);
        assert_eq!(
            once.after_timeout(PullProbe::Untried),
            PullProbe::TimedOutOnce
        );
    }

    #[test]
    fn test_a_timeout_never_undoes_an_answer_or_a_refusal() {
        for sent_with in [PullProbe::Untried, PullProbe::TimedOutOnce] {
            assert_eq!(
                PullProbe::Answered.after_timeout(sent_with),
                PullProbe::Answered
            );
            assert_eq!(
                PullProbe::Refused.after_timeout(sent_with),
                PullProbe::Refused
            );
        }
    }

    #[test]
    fn test_only_a_refusing_server_is_not_pulled() {
        for support in [
            PullSupport::Advertised,
            PullSupport::Probing,
            PullSupport::Answers,
        ] {
            assert!(support.sends_pull());
        }
        assert!(!PullSupport::Unsupported.sends_pull());
    }
}