woocraft 0.4.5

GPUI components lib for Woocraft design system.
Documentation
use std::{ops::Range, sync::Arc};

use gpui::{Entity, HighlightStyle, MouseButton, SharedString, Window};
use lsp_types::Position;
use ropey::Rope;

use super::{RopeExt as _, highlighter::HighlightTheme, state::InputState};
use crate::PopupMenu;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EditorPointerButton {
  Left,
  Right,
  Middle,
  Other,
}

impl From<MouseButton> for EditorPointerButton {
  fn from(value: MouseButton) -> Self {
    match value {
      MouseButton::Left => Self::Left,
      MouseButton::Right => Self::Right,
      MouseButton::Middle => Self::Middle,
      _ => Self::Other,
    }
  }
}

#[derive(Clone, Debug)]
pub enum EditorUserAction {
  MoveCursor {
    offset: u64,
  },
  Select {
    range: Range<u64>,
    reversed: bool,
  },
  Replace {
    range: Range<u64>,
    new_text: String,
    marked: bool,
    silent: bool,
  },
  MouseDown {
    offset: u64,
    button: EditorPointerButton,
    click_count: u8,
    shift: bool,
  },
  MouseUp {
    offset: u64,
    button: EditorPointerButton,
  },
  MouseMove {
    offset: u64,
  },
  Scroll {
    delta_x: f32,
    delta_y: f32,
  },
  Copy {
    range: Range<u64>,
  },
  Cut {
    range: Range<u64>,
  },
  Paste {
    range: Range<u64>,
    text: String,
  },
  UndoRequested,
  RedoRequested,
  ContextMenuRequested {
    offset: u64,
  },
}

#[derive(Clone, Debug)]
pub struct EditorBackendEditRequest {
  pub range: Range<u64>,
  pub new_text: String,
  pub marked: bool,
}

#[derive(Clone, Debug)]
pub struct EditorBackendEditResult {
  pub accepted: bool,
  pub selection: Option<Range<u64>>,
  pub cursor: Option<u64>,
}

impl Default for EditorBackendEditResult {
  fn default() -> Self {
    Self {
      accepted: true,
      selection: None,
      cursor: None,
    }
  }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EditorBackendCapabilities {
  pub editable: bool,
  pub custom_line_numbers: bool,
  pub custom_highlighter: bool,
}

impl Default for EditorBackendCapabilities {
  fn default() -> Self {
    Self {
      editable: true,
      custom_line_numbers: false,
      custom_highlighter: false,
    }
  }
}

impl EditorBackendCapabilities {
  pub const fn read_only() -> Self {
    Self {
      editable: false,
      custom_line_numbers: false,
      custom_highlighter: false,
    }
  }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorLine {
  pub row: u64,
  pub byte_range: Range<u64>,
  pub text: SharedString,
  pub line_number: SharedString,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorTextChange {
  pub range: Range<u64>,
  pub new_text: SharedString,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EditorEditError {
  Unsupported,
  Rejected(SharedString),
}

pub trait EditorSnapshot {
  fn revision(&self) -> u64;

  fn byte_len(&self) -> u64;

  fn line_count(&self) -> u64;

  fn line_number_text(&self, row: u64) -> SharedString {
    (row.saturating_add(1)).to_string().into()
  }

  fn max_line_number_text(&self) -> SharedString {
    self.line_number_text(self.line_count().saturating_sub(1))
  }

  fn line(&self, row: u64) -> Option<EditorLine>;

  fn lines(&self, rows: Range<u64>) -> Arc<[EditorLine]> {
    rows
      .filter_map(|row| self.line(row))
      .collect::<Vec<_>>()
      .into()
  }

  fn line_range(&self, row: u64) -> Option<Range<u64>> {
    self.line(row).map(|line| line.byte_range)
  }

  fn text_for_range(&self, range: Range<u64>) -> Option<SharedString>;

  fn rope(&self) -> Rope {
    Rope::from(
      self
        .text_for_range(0..self.byte_len())
        .unwrap_or_default()
        .as_ref(),
    )
  }

  fn position_to_offset(&self, position: &Position) -> u64;

  fn offset_to_position(&self, offset: u64) -> Position;
}

pub trait EditorHighlighter {
  fn sync(&mut self, snapshot: &dyn EditorSnapshot, change: Option<&EditorTextChange>);

  fn highlight_range(
    &self, snapshot: &dyn EditorSnapshot, range: Range<u64>, theme: &HighlightTheme,
  ) -> Vec<(Range<u64>, HighlightStyle)>;
}

pub trait EditorHighlighterProvider {
  fn create_highlighter(&self) -> Option<Box<dyn EditorHighlighter>> {
    None
  }
}

pub trait EditorActionSink {
  fn on_user_action(&mut self, _action: &EditorUserAction) {}
}

pub trait EditorContextMenuProvider {
  fn extend_context_menu(
    &self, menu: PopupMenu, _state: &Entity<InputState>, _window: &mut Window,
  ) -> PopupMenu {
    menu
  }
}

pub trait EditorBackend:
  EditorActionSink + EditorContextMenuProvider + EditorHighlighterProvider {
  fn revision(&self) -> u64;

  fn capabilities(&self) -> EditorBackendCapabilities {
    EditorBackendCapabilities::default()
  }

  fn snapshot(&self) -> Arc<dyn EditorSnapshot>;

  fn apply_edit(
    &mut self, _request: EditorBackendEditRequest,
  ) -> Result<EditorBackendEditResult, EditorEditError> {
    Err(EditorEditError::Unsupported)
  }
}

#[derive(Clone)]
pub struct RopeEditorSnapshot {
  revision: u64,
  text: Rope,
}

impl RopeEditorSnapshot {
  pub fn new(revision: u64, text: Rope) -> Self {
    Self { revision, text }
  }

  pub fn text(&self) -> &Rope {
    &self.text
  }
}

impl EditorSnapshot for RopeEditorSnapshot {
  fn revision(&self) -> u64 {
    self.revision
  }

  fn byte_len(&self) -> u64 {
    self.text.len() as u64
  }

  fn line_count(&self) -> u64 {
    self.text.lines_len() as u64
  }

  fn line(&self, row: u64) -> Option<EditorLine> {
    let row = row as usize;
    if row >= self.text.lines_len() {
      return None;
    }

    let start = self.text.line_start_offset(row) as u64;
    let end = self.text.line_end_offset(row) as u64;
    Some(EditorLine {
      row: row as u64,
      byte_range: start..end,
      text: self
        .text
        .slice(start as usize..end as usize)
        .to_string()
        .into(),
      line_number: self.line_number_text(row as u64),
    })
  }

  fn text_for_range(&self, range: Range<u64>) -> Option<SharedString> {
    let start = (range.start as usize).min(self.text.len());
    let end = (range.end as usize).min(self.text.len());
    Some(self.text.slice(start..end).to_string().into())
  }

  fn rope(&self) -> Rope {
    self.text.clone()
  }

  fn position_to_offset(&self, position: &Position) -> u64 {
    self.text.position_to_offset(position) as u64
  }

  fn offset_to_position(&self, offset: u64) -> Position {
    self
      .text
      .offset_to_position((offset as usize).min(self.text.len()))
  }
}

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

  #[test]
  fn rope_snapshot_exposes_lines_and_offsets() {
    let snapshot = RopeEditorSnapshot::new(7, Rope::from("alpha\nbeta\n"));

    assert_eq!(snapshot.revision(), 7);
    assert_eq!(snapshot.line_count(), 3);
    assert_eq!(snapshot.byte_len(), "alpha\nbeta\n".len() as u64);
    assert_eq!(snapshot.line(0).unwrap().text, SharedString::from("alpha"));
    assert_eq!(snapshot.line(1).unwrap().text, SharedString::from("beta"));
    assert_eq!(snapshot.position_to_offset(&Position::new(1, 2)), 8);
    assert_eq!(snapshot.offset_to_position(8), Position::new(1, 2));
  }
}