laburnum 1.17.1

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use std::collections::HashSet;

/// Declares a topic enum for use with the notification subscription system.
///
/// Topics control selective notification delivery: when the server broadcasts
/// a notification on a topic, only clients subscribed to that topic receive
/// it. This allows different client types (IDE, CLI, CI runner) to opt in
/// to only the notifications they care about.
///
/// This macro generates:
/// - An enum with the given variants
/// - [`Topic`] trait implementation (maps each variant to its string name)
/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash` derives
/// - [`Display`](std::fmt::Display) implementation (displays the string name)
///
/// # Usage
///
/// ```ignore
/// laburnum::define_topics! {
///     MyTopics,
///     topics = [
///         Diagnostics = "diagnostics",
///         BuildProgress = "build_progress",
///         TestProgress = "test_progress",
///         TestResults = "test_results",
///         ServerStatus = "server_status",
///     ],
/// }
///
/// // Subscribe a client
/// registry.subscribe(client_id, MyTopics::Diagnostics);
///
/// // Broadcast to subscribers only
/// registry.broadcast(MyTopics::Diagnostics, notification).await;
/// ```
#[macro_export]
macro_rules! define_topics {
  (
    $name:ident,
    topics = [
      $( $variant:ident = $str:literal ),+ $(,)?
    ] $(,)?
  ) => {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub enum $name {
      $( $variant, )+
    }

    impl $crate::connect::lsp::Topic for $name {
      fn name(&self) -> &'static str {
        match self {
          $( | $name::$variant => $str, )+
        }
      }
    }

    impl ::std::fmt::Display for $name {
      fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        f.write_str($crate::connect::lsp::Topic::name(self))
      }
    }
  };
}

/// A notification topic that clients can subscribe to.
///
/// Topics allow selective notification delivery: when the server broadcasts
/// a notification on a topic, only clients that have subscribed to that
/// topic receive it. This enables different client types (IDE, CLI, CI)
/// to receive only the notifications they care about.
///
/// Laburnum does not define any built-in topics. Implementing crates
/// declare their own topic enum using the [`define_topics!`] macro,
/// following the same pattern as [`define_partitions!`].
///
/// # Implementing
///
/// Each topic variant maps to a `&'static str` name via [`Topic::name`].
/// The name is what gets stored in the subscription set and matched
/// during broadcast — it must be unique within the implementing crate's
/// topic enum.
///
/// Use the [`define_topics!`] macro rather than implementing this trait
/// manually:
///
/// ```ignore
/// laburnum::define_topics! {
///     MyTopics,
///     topics = [
///         Diagnostics = "diagnostics",
///         BuildProgress = "build_progress",
///         ServerStatus = "server_status",
///     ],
/// }
///
/// // Subscribe a client to diagnostics
/// registry.subscribe(client_id, MyTopics::Diagnostics);
///
/// // Broadcast — only subscribers receive the notification
/// registry.broadcast(MyTopics::Diagnostics, notification).await;
/// ```
///
/// [`define_topics!`]: crate::define_topics
/// [`define_partitions!`]: crate::define_partitions
pub trait Topic: Copy + Send + Sync + 'static {
  /// Human-readable name for this topic.
  ///
  /// Used as the key for subscription matching: a client subscribing to
  /// a topic stores this string, and [`ClientRegistry::broadcast`]
  /// checks it when deciding which clients receive a notification.
  ///
  /// [`ClientRegistry::broadcast`]: crate::connect::lsp::ClientRegistry::broadcast
  fn name(&self) -> &'static str;
}

/// Tracks which notification topics a client has subscribed to.
///
/// Internally stores topic names as `&'static str` so that the type
/// remains non-generic — no type parameter propagates through
/// [`ConnectedClient`], [`ClientRegistry`], or the scheduler.
///
/// [`ConnectedClient`]: crate::connect::lsp::ConnectedClient
/// [`ClientRegistry`]: crate::connect::lsp::ClientRegistry
#[derive(Debug, Clone, Default)]
pub struct Subscriptions {
  topics: HashSet<&'static str>,
}

impl Subscriptions {
  pub fn new() -> Self {
    Self {
      topics: HashSet::new(),
    }
  }

  pub fn subscribe(&mut self, topic: impl Topic) {
    self.topics.insert(topic.name());
  }

  pub fn unsubscribe(&mut self, topic: impl Topic) {
    self.topics.remove(topic.name());
  }

  pub fn is_subscribed(&self, topic: impl Topic) -> bool {
    self.topics.contains(topic.name())
  }

  pub fn topics(&self) -> impl Iterator<Item = &'static str> + '_ {
    self.topics.iter().copied()
  }

  pub fn clear(&mut self) {
    self.topics.clear();
  }
}

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

  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  enum TestTopic {
    Alpha,
    Beta,
  }

  impl Topic for TestTopic {
    fn name(&self) -> &'static str {
      match self {
        | TestTopic::Alpha => "alpha",
        | TestTopic::Beta => "beta",
      }
    }
  }

  impl std::fmt::Display for TestTopic {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
      f.write_str(self.name())
    }
  }

  #[test]
  fn topic_display() {
    assert_eq!(format!("{}", TestTopic::Alpha), "alpha");
    assert_eq!(format!("{}", TestTopic::Beta), "beta");
  }

  #[test]
  fn subscriptions_subscribe_unsubscribe() {
    let mut subs = Subscriptions::new();

    assert!(!subs.is_subscribed(TestTopic::Alpha));

    subs.subscribe(TestTopic::Alpha);
    assert!(subs.is_subscribed(TestTopic::Alpha));

    subs.unsubscribe(TestTopic::Alpha);
    assert!(!subs.is_subscribed(TestTopic::Alpha));
  }

  #[test]
  fn subscriptions_multiple_topics() {
    let mut subs = Subscriptions::new();
    subs.subscribe(TestTopic::Alpha);
    subs.subscribe(TestTopic::Beta);

    assert!(subs.is_subscribed(TestTopic::Alpha));
    assert!(subs.is_subscribed(TestTopic::Beta));
  }

  #[test]
  fn subscriptions_clear() {
    let mut subs = Subscriptions::new();
    subs.subscribe(TestTopic::Alpha);
    subs.subscribe(TestTopic::Beta);
    subs.clear();

    assert!(!subs.is_subscribed(TestTopic::Alpha));
    assert!(!subs.is_subscribed(TestTopic::Beta));
  }
}