use gpui::{
App, Context, CursorStyle, ElementInputHandler, EventEmitter, FocusHandle, Focusable,
KeyContext, MouseButton, Render, Styled as _, Task, Window, canvas, div, prelude::*,
};
use markdown::{
Annotation, AppExt as _, Block, BlockKind, BlockLayouts, Cursor, Doc, Form, Mark, Part,
Selection, Splice, Text, edit, edit::shortcut,
};
use std::{ops::Range, time::Duration};
use theme::Theme;
use crate::{
anchor::{Anchor, AnchorId, Delta},
history::{EditKind, History},
link::{self, Choice},
slash::Slash,
text_size,
};
mod anchors;
mod blocks;
mod caret;
mod clipboard;
pub(crate) mod image;
mod input;
pub mod keys;
pub(crate) mod menu;
mod mode;
mod pointer;
mod render;
mod table;
mod typing;
pub use keys::init;
use keys::{
Backspace, Copy, Cut, DecreaseTextSize, Delete, DeleteToHome, DeleteWordLeft, DeleteWordRight,
Dismiss, DocumentEnd, DocumentStart, Down, DuplicateBlock, End, Home, IncreaseTextSize, Indent,
InsertParagraph, KillLine, Left, MoveBlockDown, MoveBlockUp, Outdent, Paste, Redo, RemoveBlock,
ResetTextSize, Right, SelectAll, SelectDocumentEnd, SelectDocumentStart, SelectDown, SelectEnd,
SelectHome, SelectLeft, SelectRight, SelectUp, SelectWordLeft, SelectWordRight, SoftBreak,
SplitBlock, ToggleBold, ToggleCode, ToggleHighlight, ToggleItalic, ToggleStrike, Undo, Up,
WordLeft, WordRight,
};
pub const CONTEXT: &str = "BezelEditor";
pub const HIGHLIGHT_MARK: &str = "highlight";
fn key_context() -> KeyContext {
let mut context = KeyContext::default();
context.add(CONTEXT);
context.add(ui::focus::CLAIMS_TAB);
context
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum EditorEvent {
Changed,
AnchorActivated(AnchorId),
ModeChanged(Mode),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Mode {
#[default]
Blocks,
Source,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Chrome {
pub handle: bool,
pub slash: bool,
pub language: bool,
pub paste: bool,
}
impl Default for Chrome {
fn default() -> Self {
Self {
handle: true,
slash: true,
language: true,
paste: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Formatting {
pub mode: Mode,
pub marks: Vec<Mark>,
pub block: Option<gpui::SharedString>,
pub fenceable: bool,
}
pub fn turns() -> Vec<(gpui::SharedString, BlockKind)> {
crate::slash::items()
}
const PLACEHOLDER: &str = "Type / for commands";
const INDENT: &str = " ";
const BLINK: Duration = Duration::from_millis(500);
const HANDLE_SIZE: f32 = 18.0;
const MIN_IMAGE_WIDTH: f32 = 96.0;
fn fenceable(doc: &Doc, selection: Selection) -> bool {
let spans = doc.spans(selection);
let covered = |at: &Cursor, range: &Range<usize>| {
doc.blocks[at.block]
.text_at(at.part)
.and_then(|text| text.text.get(range.clone()))
};
spans.iter().all(|(at, _)| at.part == Part::Body)
&& (spans.len() > 1
|| spans
.iter()
.any(|(at, range)| covered(at, range).is_some_and(|text| text.contains('\n'))))
}
fn ensure_block(doc: &mut Doc) -> bool {
if !doc.blocks.is_empty() {
return false;
}
doc.blocks
.push(Block::new(BlockKind::Paragraph(Text::default())));
true
}
fn line_home(at: Cursor, doc: &Doc) -> Cursor {
let Some(text) = doc
.blocks
.get(at.block)
.and_then(|block| block.text_at(at.part))
else {
return at.home();
};
Cursor {
offset: ui::input::line_start(&text.text, at.offset.min(text.text.len())),
..at
}
}
fn line_end(at: Cursor, doc: &Doc) -> Cursor {
let Some(text) = doc
.blocks
.get(at.block)
.and_then(|block| block.text_at(at.part))
else {
return at.end(doc);
};
Cursor {
offset: ui::input::line_end(&text.text, at.offset.min(text.text.len())),
..at
}
}
fn source_doc(source: &str) -> Doc {
Doc {
blocks: vec![Block::new(BlockKind::Code {
language: Some(markdown::source::LANGUAGES[0].to_string()),
code: Text::plain(source),
})],
}
}
pub(crate) type MenuPopup = ui::popover::Popup<(usize, gpui::Point<gpui::Pixels>)>;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum TableTarget {
Row(usize),
Column(usize),
Cell { row: usize, column: usize },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Line {
Row(usize),
Column(usize),
}
pub struct Editor {
doc: Doc,
marks: markdown::Marks,
chrome: Chrome,
mode: Mode,
selection: Selection,
focus_handle: FocusHandle,
marked: Option<Range<usize>>,
layouts: BlockLayouts,
history: History,
caret_on: bool,
blink: Option<Task<()>>,
anchors: Vec<Anchor>,
stored: Vec<Mark>,
slash: Option<Slash>,
pasted: Option<link::Paste>,
url_prompt: Option<image::Prompt>,
dropping: Option<usize>,
hovered: Option<usize>,
hovered_cell: Option<(usize, Part)>,
table_drag: Option<table::TableDrag>,
table_dragged: bool,
table_menu: ui::popover::Popup<(usize, TableTarget, gpui::Point<gpui::Pixels>)>,
lifted: Option<(usize, usize)>,
resizing: Option<(usize, Option<u32>)>,
block_menu: MenuPopup,
language_menu: MenuPopup,
press_claimed: bool,
origin: gpui::Point<gpui::Pixels>,
width: gpui::Pixels,
dragging: bool,
over_text: bool,
scroll: Option<gpui::ScrollHandle>,
reveal: bool,
handle_at: Option<gpui::Point<gpui::Pixels>>,
goal: Option<VerticalGoal>,
text_size: Option<f32>,
base: Option<std::path::PathBuf>,
image_overlay: Option<markdown::ImageOverlay>,
image_overlay_corner: markdown::ImageOverlayCorner,
}
#[derive(Clone, Copy)]
struct VerticalGoal {
x: gpui::Pixels,
row_from_caret: gpui::Pixels,
}
impl Editor {
pub fn new(source: &str, cx: &mut Context<Self>) -> Self {
let marks = cx.marks();
let mut doc = markdown::parse_with(source, &marks);
ensure_block(&mut doc);
Self {
marks,
selection: Selection::at(Cursor::default().clamp(&doc)),
doc,
chrome: Chrome::default(),
mode: Mode::default(),
focus_handle: cx.focus_handle(),
marked: None,
layouts: BlockLayouts::default(),
history: History::default(),
caret_on: true,
blink: None,
anchors: Vec::new(),
stored: Vec::new(),
slash: None,
pasted: None,
url_prompt: None,
dropping: None,
hovered: None,
hovered_cell: None,
table_menu: Default::default(),
table_drag: None,
table_dragged: false,
lifted: None,
resizing: None,
block_menu: MenuPopup::default(),
language_menu: MenuPopup::default(),
press_claimed: false,
origin: gpui::Point::default(),
width: gpui::Pixels::ZERO,
dragging: false,
over_text: false,
scroll: None,
reveal: false,
goal: None,
handle_at: None,
text_size: None,
base: None,
image_overlay: None,
image_overlay_corner: markdown::ImageOverlayCorner::default(),
}
}
pub fn with_undo_limit(mut self, limit: usize) -> Self {
self.history = History::with_limit(limit);
self
}
pub fn with_marks(mut self, marks: markdown::Marks) -> Self {
let source = self.source();
self.marks = marks;
self.doc = markdown::parse_with(&source, &self.marks);
ensure_block(&mut self.doc);
self.selection = self.selection.clamp(&self.doc);
self
}
pub fn with_chrome(mut self, chrome: Chrome) -> Self {
self.chrome = chrome;
self
}
pub fn with_image_overlay(mut self, overlay: markdown::ImageOverlay) -> Self {
self.image_overlay = Some(overlay);
self
}
pub fn set_image_overlay(
&mut self,
overlay: Option<markdown::ImageOverlay>,
cx: &mut Context<Self>,
) {
self.image_overlay = overlay;
cx.notify();
}
pub fn with_image_overlay_corner(mut self, corner: markdown::ImageOverlayCorner) -> Self {
self.image_overlay_corner = corner;
self
}
pub fn set_image_overlay_corner(
&mut self,
corner: markdown::ImageOverlayCorner,
cx: &mut Context<Self>,
) {
self.image_overlay_corner = corner;
cx.notify();
}
pub fn chrome(&self) -> Chrome {
self.chrome
}
pub fn with_mode(mut self, mode: Mode) -> Self {
if mode != self.mode {
self.switch(mode);
}
self
}
pub fn with_text_size(mut self, points: f32) -> Self {
self.text_size = Some(points);
self
}
pub fn set_text_size(&mut self, points: f32, cx: &mut Context<Self>) {
if self.text_size != Some(points) {
self.text_size = Some(points);
cx.notify();
}
}
pub fn text_size(&self) -> Option<f32> {
self.text_size
}
pub fn with_base(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
self.base = Some(dir.into());
self
}
pub fn set_base(&mut self, dir: Option<std::path::PathBuf>, cx: &mut Context<Self>) {
if self.base != dir {
self.base = dir;
cx.notify();
}
}
pub fn base(&self) -> Option<&std::path::Path> {
self.base.as_deref()
}
pub fn with_scroll(mut self, handle: gpui::ScrollHandle) -> Self {
self.scroll = Some(handle);
self
}
pub fn doc(&self) -> &Doc {
&self.doc
}
pub fn selection(&self) -> Selection {
self.selection
}
pub fn select(&mut self, selection: Selection, cx: &mut Context<Self>) {
self.selection = selection.clamp(&self.doc);
self.history.interrupt();
self.reveal = true;
self.caret_moved();
cx.notify();
}
pub fn layouts(&self) -> &BlockLayouts {
&self.layouts
}
pub fn selection_bounds(&self) -> Option<gpui::Bounds<gpui::Pixels>> {
if self.selection.is_collapsed() {
return None;
}
let (point, line_height) = self.layouts.position(self.selection.head)?;
Some(gpui::Bounds::new(
point,
gpui::size(gpui::px(0.0), line_height),
))
}
}
impl EventEmitter<EditorEvent> for Editor {}
impl Focusable for Editor {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}