woocraft 0.4.5

GPUI components lib for Woocraft design system.
Documentation
use gpui::{Context, Window};

use crate::widgets::editor::{
  EditorUserAction, InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp,
  MoveRight, MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
};

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum MoveDirection {
  Up,
  Down,
}

impl InputState {
  /// Called after moving the cursor. Updates preferred_column if we know where
  /// the cursor now is.
  pub(super) fn update_preferred_column(&mut self) {
    let Some(last_layout) = &self.last_layout else {
      self.preferred_column = None;
      return;
    };

    let point = self.text.offset_to_point(self.cursor());
    let Some(line) = last_layout.line(point.row) else {
      self.preferred_column = None;
      return;
    };

    let Some(pos) = line.position_for_index(point.column, last_layout) else {
      self.preferred_column = None;
      return;
    };

    self.preferred_column = Some((pos.x, point.column));
  }

  /// Move the cursor to the given offset.
  ///
  /// The offset is the UTF-8 offset.
  ///
  /// Ensure the offset use self.next_boundary or self.previous_boundary to get
  /// the correct offset.
  pub(crate) fn move_to(
    &mut self, offset: usize, direction: Option<MoveDirection>, cx: &mut Context<Self>,
  ) {
    let offset = offset.clamp(0, self.text.len());
    self.selected_range = (offset..offset).into();
    self.emit_backend_action(EditorUserAction::MoveCursor {
      offset: offset as u64,
    });
    self.scroll_to(offset, direction, cx);
    self.pause_blink_cursor(cx);
    self.update_preferred_column();
    self.hide_context_menu(cx);
    self.clear_inline_completion(cx);
    cx.notify()
  }

  /// Move the cursor vertically by one line (up or down) while preserving the
  /// column if possible.
  ///
  /// move_lines: Number of lines to move vertically (positive for down,
  /// negative for up).
  pub(super) fn move_vertical(
    &mut self, move_lines: isize, _: &mut Window, cx: &mut Context<Self>,
  ) {
    let offset = self.cursor();
    let was_preferred_column = self.preferred_column;

    let point = self.text.offset_to_point(offset);
    let total_rows = self.text.lines_len();
    if total_rows == 0 {
      return;
    }

    let next_row = point
      .row
      .saturating_add_signed(move_lines)
      .min(total_rows.saturating_sub(1));
    let line_start_offset = self.text.line_start_offset(next_row);
    let max_line_len = self.text.slice_line(next_row).len();
    let preferred_column = was_preferred_column
      .map(|(_, column)| column)
      .unwrap_or(point.column);
    let new_offset = line_start_offset + preferred_column.min(max_line_len);

    self.pause_blink_cursor(cx);
    let direction = if move_lines < 0 {
      MoveDirection::Up
    } else {
      MoveDirection::Down
    };
    self.move_to(new_offset, Some(direction), cx);
    // Set back the preferred_column
    self.preferred_column = was_preferred_column;
    cx.notify();
  }

  pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
    self.pause_blink_cursor(cx);
    if self.selected_range.is_empty() {
      self.move_to(self.previous_boundary(self.cursor()), None, cx);
    } else {
      self.move_to(self.selected_range.start, None, cx)
    }
  }

  pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
    self.pause_blink_cursor(cx);
    if self.selected_range.is_empty() {
      self.move_to(self.next_boundary(self.selected_range.end), None, cx);
    } else {
      self.move_to(self.selected_range.end, None, cx)
    }
  }

  pub(super) fn up(&mut self, action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
    if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
      return;
    }

    if !self.selected_range.is_empty() {
      self.move_to(
        self.previous_boundary(self.selected_range.start.saturating_sub(1)),
        Some(MoveDirection::Up),
        cx,
      );
    }
    self.pause_blink_cursor(cx);
    self.move_vertical(-1, window, cx);
  }

  pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
    if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
      return;
    }

    if !self.selected_range.is_empty() {
      self.move_to(
        self.next_boundary(self.selected_range.end.saturating_sub(1)),
        Some(MoveDirection::Down),
        cx,
      );
    }

    self.pause_blink_cursor(cx);
    self.move_vertical(1, window, cx);
  }

  pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
    let Some(last_layout) = &self.last_layout else {
      return;
    };

    let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
    self.move_vertical(-display_lines, window, cx);
  }

  pub(super) fn page_down(
    &mut self, _: &MovePageDown, window: &mut Window, cx: &mut Context<Self>,
  ) {
    let Some(last_layout) = &self.last_layout else {
      return;
    };

    let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
    self.move_vertical(display_lines, window, cx);
  }

  pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
    self.pause_blink_cursor(cx);
    let offset = self.start_of_line();
    self.move_to(offset, Some(MoveDirection::Up), cx);
  }

  pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
    self.pause_blink_cursor(cx);
    let offset = self.end_of_line();
    self.move_to(offset, Some(MoveDirection::Down), cx);
  }

  pub(super) fn move_to_start(&mut self, _: &MoveToStart, _: &mut Window, cx: &mut Context<Self>) {
    self.move_to(0, None, cx);
  }

  pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
    self.move_to(self.text.len(), None, cx);
  }

  pub(super) fn move_to_previous_word(
    &mut self, _: &MoveToPreviousWord, _: &mut Window, cx: &mut Context<Self>,
  ) {
    let offset = self.previous_start_of_word();
    self.move_to(offset, None, cx);
  }

  pub(super) fn move_to_next_word(
    &mut self, _: &MoveToNextWord, _: &mut Window, cx: &mut Context<Self>,
  ) {
    let offset = self.next_end_of_word();
    self.move_to(offset, None, cx);
  }
}