woocraft 0.4.5

GPUI components lib for Woocraft design system.
Documentation
use std::{
  cmp::Ordering,
  ops::{Deref, Range},
};

use gpui::{App, HighlightStyle, Hsla, SharedString, UnderlineStyle, px};
use ropey::Rope;
use sum_tree::{Bias, SeekTarget, SumTree};

use crate::{
  ActiveTheme, ColorExt as _,
  widgets::editor::{Position, RopeExt as _},
};

pub type DiagnosticRelatedInformation = lsp_types::DiagnosticRelatedInformation;
pub type CodeDescription = lsp_types::CodeDescription;
pub type RelatedInformation = lsp_types::DiagnosticRelatedInformation;
pub type DiagnosticTag = lsp_types::DiagnosticTag;

#[derive(Debug, Eq, PartialEq, Clone, Default)]
pub struct Diagnostic {
  /// The range [`Position`] at which the message applies.
  ///
  /// This is the column, character range within a single line.
  pub range: Range<Position>,

  /// The diagnostic's severity. Can be omitted. If omitted it is up to the
  /// client to interpret diagnostics as error, warning, info or hint.
  pub severity: DiagnosticSeverity,

  /// The diagnostic's code. Can be omitted.
  pub code: Option<SharedString>,

  pub code_description: Option<CodeDescription>,

  /// A human-readable string describing the source of this
  /// diagnostic, e.g. 'typescript' or 'super lint'.
  pub source: Option<SharedString>,

  /// The diagnostic's message.
  pub message: SharedString,

  /// An array of related diagnostic information, e.g. when symbol-names within
  /// a scope collide all definitions can be marked via this property.
  pub related_information: Option<Vec<DiagnosticRelatedInformation>>,

  /// Additional metadata about the diagnostic.
  pub tags: Option<Vec<DiagnosticTag>>,

  /// A data entry field that is preserved between a
  /// `textDocument/publishDiagnostics` notification and
  /// `textDocument/codeAction` request.
  ///
  /// @since 3.16.0
  pub data: Option<serde_json::Value>,
}

impl From<lsp_types::Diagnostic> for Diagnostic {
  fn from(value: lsp_types::Diagnostic) -> Self {
    Self {
      range: value.range.start..value.range.end,
      severity: value
        .severity
        .map(Into::into)
        .unwrap_or(DiagnosticSeverity::Info),
      code: value.code.map(|c| match c {
        lsp_types::NumberOrString::Number(n) => SharedString::from(n.to_string()),
        lsp_types::NumberOrString::String(s) => SharedString::from(s),
      }),
      code_description: value.code_description,
      source: value.source.map(|s| s.into()),
      message: value.message.into(),
      related_information: value.related_information,
      tags: value.tags,
      data: value.data,
    }
  }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DiagnosticSeverity {
  #[default]
  Hint,
  Error,
  Warning,
  Info,
}

impl From<lsp_types::DiagnosticSeverity> for DiagnosticSeverity {
  fn from(value: lsp_types::DiagnosticSeverity) -> Self {
    match value {
      lsp_types::DiagnosticSeverity::ERROR => Self::Error,
      lsp_types::DiagnosticSeverity::WARNING => Self::Warning,
      lsp_types::DiagnosticSeverity::INFORMATION => Self::Info,
      lsp_types::DiagnosticSeverity::HINT => Self::Hint,
      _ => Self::Info, // Default to Info if unknown
    }
  }
}

impl DiagnosticSeverity {
  pub(crate) fn bg(&self, cx: &App) -> Hsla {
    let theme = cx.theme();
    let bg = theme.background;
    match self {
      Self::Error => bg.blend(theme.red.alpha(0.2)),
      Self::Warning => bg.blend(theme.yellow.alpha(0.2)),
      Self::Info => bg.blend(theme.blue.alpha(0.2)),
      Self::Hint => bg.blend(theme.cyan.alpha(0.2)),
    }
  }

  pub(crate) fn fg(&self, cx: &App) -> Hsla {
    let theme = cx.theme();
    match self {
      Self::Error => theme.red,
      Self::Warning => theme.yellow,
      Self::Info => theme.blue,
      Self::Hint => theme.cyan,
    }
  }

  pub(crate) fn border(&self, cx: &App) -> Hsla {
    let theme = cx.theme();
    match self {
      Self::Error => theme.red,
      Self::Warning => theme.yellow,
      Self::Info => theme.blue,
      Self::Hint => theme.cyan,
    }
  }

  pub(crate) fn highlight_style(&self, cx: &App) -> HighlightStyle {
    let color = match self {
      Self::Error => Some(cx.theme().red),
      Self::Warning => Some(cx.theme().yellow),
      Self::Info => Some(cx.theme().blue),
      Self::Hint => Some(cx.theme().cyan),
    };

    HighlightStyle {
      underline: Some(UnderlineStyle {
        color,
        thickness: px(1.),
        wavy: true,
      }),
      ..Default::default()
    }
  }
}

impl Diagnostic {
  pub fn new(range: Range<impl Into<Position>>, message: impl Into<SharedString>) -> Self {
    Self {
      range: range.start.into()..range.end.into(),
      message: message.into(),
      ..Default::default()
    }
  }

  pub fn with_severity(mut self, severity: impl Into<DiagnosticSeverity>) -> Self {
    self.severity = severity.into();
    self
  }

  pub fn with_code(mut self, code: impl Into<SharedString>) -> Self {
    self.code = Some(code.into());
    self
  }

  pub fn with_source(mut self, source: impl Into<SharedString>) -> Self {
    self.source = Some(source.into());
    self
  }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct DiagnosticEntry {
  /// The byte range of the diagnostic in the rope.
  pub range: Range<usize>,
  pub diagnostic: Diagnostic,
}

impl Deref for DiagnosticEntry {
  type Target = Diagnostic;

  fn deref(&self) -> &Self::Target {
    &self.diagnostic
  }
}

#[derive(Debug, Default, Clone)]
pub struct DiagnosticSummary {
  count: usize,
  start: usize,
  end: usize,
}

impl sum_tree::Item for DiagnosticEntry {
  type Summary = DiagnosticSummary;
  fn summary(&self, _cx: &()) -> Self::Summary {
    DiagnosticSummary {
      count: 1,
      start: self.range.start,
      end: self.range.end,
    }
  }
}

impl sum_tree::Summary for DiagnosticSummary {
  type Context<'a> = &'a ();
  fn zero(_: Self::Context<'_>) -> Self {
    DiagnosticSummary {
      count: 0,
      start: usize::MIN,
      end: usize::MIN,
    }
  }

  fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) {
    self.start = other.start;
    self.end = other.end;
    self.count += other.count;
  }
}

/// For seeking by byte range.
impl SeekTarget<'_, DiagnosticSummary, DiagnosticSummary> for usize {
  fn cmp(&self, other: &DiagnosticSummary, _: &()) -> Ordering {
    if *self < other.start {
      Ordering::Less
    } else if *self > other.end {
      Ordering::Greater
    } else {
      Ordering::Equal
    }
  }
}

#[derive(Debug, Clone)]
pub struct DiagnosticSet {
  text: Rope,
  diagnostics: SumTree<DiagnosticEntry>,
}

impl DiagnosticSet {
  pub fn new(text: &Rope) -> Self {
    Self {
      text: text.clone(),
      diagnostics: SumTree::new(&()),
    }
  }

  pub fn reset(&mut self, text: &Rope) {
    self.text = text.clone();
    self.clear();
  }

  pub fn push(&mut self, diagnostic: impl Into<Diagnostic>) {
    let diagnostic = diagnostic.into();
    let start = self.text.position_to_offset(&diagnostic.range.start);
    let end = self.text.position_to_offset(&diagnostic.range.end);

    self.diagnostics.push(
      DiagnosticEntry {
        range: start..end,
        diagnostic,
      },
      &(),
    );
  }

  pub fn extend<D, I>(&mut self, diagnostics: D)
  where
    D: IntoIterator<Item = I>,
    I: Into<Diagnostic>, {
    for diagnostic in diagnostics {
      self.push(diagnostic.into());
    }
  }

  pub fn len(&self) -> usize {
    self.diagnostics.summary().count
  }

  pub fn clear(&mut self) {
    self.diagnostics = SumTree::new(&());
  }

  pub fn is_empty(&self) -> bool {
    self.diagnostics.is_empty()
  }

  pub(crate) fn range(&self, range: Range<usize>) -> impl Iterator<Item = &DiagnosticEntry> {
    let mut cursor = self.diagnostics.cursor::<DiagnosticSummary>(&());
    cursor.seek(&range.start, Bias::Left);
    std::iter::from_fn(move || {
      if let Some(entry) = cursor.item()
        && entry.range.start < range.end
      {
        cursor.next();
        return Some(entry);
      }
      None
    })
  }

  pub(crate) fn for_offset(&self, offset: usize) -> Option<&DiagnosticEntry> {
    self.range(offset..offset + 1).next()
  }

  pub(crate) fn styles_for_range(
    &self, range: &Range<usize>, cx: &App,
  ) -> Vec<(Range<usize>, HighlightStyle)> {
    if self.diagnostics.is_empty() {
      return vec![];
    }

    let mut styles = vec![];
    for entry in self.range(range.clone()) {
      let range = entry.range.clone();
      styles.push((range, entry.diagnostic.severity.highlight_style(cx)));
    }

    styles
  }

  #[allow(unused)]
  pub(crate) fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry> {
    self.diagnostics.iter()
  }
}

#[cfg(test)]
mod tests {
  use crate::widgets::editor::Position;

  #[test]
  fn test_diagnostic() {
    use ropey::Rope;

    use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity};

    let text = Rope::from("Hello, 你好warld!\nThis is a test.\nGoodbye, world!");
    let mut diagnostics = DiagnosticSet::new(&text);

    diagnostics.push(
      Diagnostic::new(
        Position::new(0, 7)..Position::new(0, 17),
        "Spelling mistake",
      )
      .with_severity(DiagnosticSeverity::Warning),
    );
    diagnostics.push(
      Diagnostic::new(Position::new(2, 9)..Position::new(2, 14), "Syntax error")
        .with_severity(DiagnosticSeverity::Error),
    );

    assert_eq!(diagnostics.len(), 2);
    let items = diagnostics.iter().collect::<Vec<_>>();

    assert_eq!(items[0].message.as_str(), "Spelling mistake");
    assert_eq!(items[0].range, 7..19);

    assert_eq!(items[1].message.as_str(), "Syntax error");
    assert_eq!(items[1].range, 45..50);

    let items = diagnostics.range(6..48).collect::<Vec<_>>();
    assert_eq!(items.len(), 2);

    let item = diagnostics.for_offset(10).unwrap();
    assert_eq!(item.message.as_str(), "Spelling mistake");

    let item = diagnostics.for_offset(30);
    assert!(item.is_none());

    let item = diagnostics.for_offset(46).unwrap();
    assert_eq!(item.message.as_str(), "Syntax error");

    diagnostics.push(
      Diagnostic::new(Position::new(1, 5)..Position::new(1, 7), "Info message")
        .with_severity(DiagnosticSeverity::Info),
    );
    assert_eq!(diagnostics.len(), 3);

    diagnostics.clear();
    assert_eq!(diagnostics.len(), 0);
  }
}