use crate::{
anchor::{Anchor, AnchorId, Delta},
document::{Change, Document, DocumentEvent, Tracked},
history::{EditKind, Run},
link::{self, Choice},
slash::{Slash, SlashAction, SlashAt},
text_size,
};
use gpui::{
App, Context, CursorStyle, ElementInputHandler, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, KeyContext, MouseButton, Render, SharedString, Styled as _, Task, Window, canvas, div,
prelude::*,
};
use markdown::{
Affinity, Annotation, AppExt as _, Block, BlockKind, BlockLayouts, Cursor, Doc, Form, Mark,
Part, Selection, Splice, Synced, Text, Typography, edit, edit::shortcut,
};
use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
use theme::Theme;
mod anchors;
mod blocks;
mod caret;
mod clipboard;
pub(crate) mod image;
mod input;
pub mod keys;
pub(crate) mod menu;
pub(crate) mod mode;
mod pointer;
mod render;
mod sizing;
mod sync;
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),
AnchorHovered(Option<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,
pub mention: bool,
}
impl Default for Chrome {
fn default() -> Self {
Self {
handle: true,
slash: true,
language: true,
paste: true,
mention: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Formatting {
pub mode: Mode,
pub marks: Vec<Mark>,
pub block: Option<SharedString>,
pub quote: Option<markdown::Quoted>,
pub fenceable: bool,
}
pub fn turns() -> Vec<(gpui::SharedString, BlockKind)> {
crate::slash::items()
}
pub fn quotes() -> Vec<(gpui::SharedString, Option<markdown::QuoteKind>)> {
crate::slash::quotes()
}
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 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),
height: None,
})],
}
}
pub(crate) type MenuPopup<T> = ui::popover::Popup<Dropped<T>>;
pub(crate) struct Dropped<T> {
pub of: T,
pub at: gpui::Point<gpui::Pixels>,
pub cursor: ui::menu::Cursor,
}
impl<T> Dropped<T> {
pub fn new(of: T, at: gpui::Point<gpui::Pixels>) -> Self {
Self {
of,
at,
cursor: ui::menu::Cursor::default(),
}
}
}
#[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),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Reveal {
Nearest,
Top,
}
pub struct Editor {
document: Entity<Document>,
doc: Tracked,
source_view: Option<(u64, markdown::SourceText)>,
source_spans: Option<Task<()>>,
marks: markdown::Marks,
synced: Rc<RefCell<Synced>>,
chrome: Chrome,
placeholder: Option<SharedString>,
mode: Mode,
selection: Selection,
focus_handle: FocusHandle,
marked: Option<Range<usize>>,
layouts: BlockLayouts,
run: Run,
caret_on: bool,
blink: Option<Task<()>>,
anchors: Vec<Anchor>,
washes: Vec<(Selection, Annotation)>,
stored: Vec<Mark>,
slash: Option<Slash>,
mention: Option<crate::mention::MentionMenu>,
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: MenuPopup<(usize, TableTarget)>,
text_menu: MenuPopup<Vec<ui::menu::Item>>,
image_menu: MenuPopup<markdown::PictureMenu>,
lifted: Option<(usize, usize)>,
resizing: Option<(usize, Option<u32>)>,
sizing: Option<sizing::Sizing>,
block_menu: MenuPopup<(usize, crate::slash::Rows)>,
language_menu: MenuPopup<usize>,
press_claimed: bool,
origin: gpui::Point<gpui::Pixels>,
width: gpui::Pixels,
dragging: Option<(ui::input::Granularity, Range<Cursor>)>,
over_text: bool,
over_anchor: Option<AnchorId>,
scroll: Option<gpui::ScrollHandle>,
reveal: Option<Reveal>,
drag_at: Option<gpui::Point<gpui::Pixels>>,
edge_scroll: Option<Task<()>>,
handle_at: Option<gpui::Point<gpui::Pixels>>,
goal: Option<gpui::Pixels>,
text_size: Option<f32>,
pub typography: Option<Typography>,
pub text_color: Option<Hsla>,
base: Option<std::path::PathBuf>,
image_overlay: Option<markdown::ImageOverlay>,
image_overlay_corner: markdown::ImageOverlayCorner,
_document: gpui::Subscription,
}
impl Editor {
pub fn new(source: &str, cx: &mut Context<Self>) -> Self {
let document = cx.new(|cx| Document::new(source, cx));
Self::for_document(document, cx)
}
pub fn for_document(document: Entity<Document>, cx: &mut Context<Self>) -> Self {
let (doc, mode, marks, synced, anchors) = {
let document = document.read(cx);
(
document.doc.clone(),
document.mode,
document.marks.clone(),
document.synced.clone(),
document.anchors.clone(),
)
};
let subscription = cx.subscribe(&document, |this, document, event: &DocumentEvent, cx| {
if event.by != Some(cx.entity_id()) {
this.take_document(&document, &event.change, cx);
}
});
Self {
document,
marks,
selection: Selection::at(Cursor::default().clamp(&doc)),
doc,
mode,
synced,
anchors,
source_view: None,
source_spans: None,
chrome: Chrome::default(),
placeholder: None,
focus_handle: cx.focus_handle(),
marked: None,
layouts: BlockLayouts::default(),
run: Run::default(),
caret_on: true,
blink: None,
washes: Vec::new(),
stored: Vec::new(),
slash: None,
mention: None,
pasted: None,
url_prompt: None,
dropping: None,
hovered: None,
hovered_cell: None,
table_menu: Default::default(),
text_menu: Default::default(),
image_menu: Default::default(),
table_drag: None,
table_dragged: false,
lifted: None,
resizing: None,
sizing: None,
block_menu: MenuPopup::default(),
language_menu: MenuPopup::default(),
press_claimed: false,
origin: gpui::Point::default(),
width: gpui::Pixels::ZERO,
dragging: None,
over_text: false,
over_anchor: None,
scroll: None,
reveal: None,
drag_at: None,
edge_scroll: None,
goal: None,
handle_at: None,
text_size: None,
typography: None,
text_color: None,
base: None,
image_overlay: None,
image_overlay_corner: markdown::ImageOverlayCorner::default(),
_document: subscription,
}
}
pub fn document(&self) -> &Entity<Document> {
&self.document
}
pub fn with_chrome(mut self, chrome: Chrome) -> Self {
self.chrome = chrome;
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = Some(placeholder.into());
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_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 resolved_typography(&self, cx: &App) -> Typography {
self.typography.unwrap_or_else(|| {
cx.typography()
.scaled(text_size::resolve(self.text_size, cx) / theme::base_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.select_revealing(selection, Reveal::Nearest, cx);
}
pub fn select_to_top(&mut self, selection: Selection, cx: &mut Context<Self>) {
self.select_revealing(selection, Reveal::Top, cx);
}
fn select_revealing(&mut self, selection: Selection, reveal: Reveal, cx: &mut Context<Self>) {
self.selection = selection.clamp(&self.doc);
self.run.interrupt();
self.reveal = Some(reveal);
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, self.selection.affinity)?;
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()
}
}