use std::ops::Range;
use gpui_kit::base::text::TextView;
use gpui_kit::component::input::{EditorState, Escape, InputEvent, Textarea, TextareaState};
use gpui_kit::component::{ActiveTheme as _, v_flex};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use crate::highlight;
use crate::insert;
use crate::theme::Palette;
pub enum RenderedEvent {
SlashTyped,
}
struct ActiveBlock {
start: usize,
committed_len: usize,
editor: Entity<TextareaState>,
_subscriptions: Vec<Subscription>,
}
pub struct RenderedView {
source: Entity<EditorState>,
active: Option<ActiveBlock>,
blocks: Vec<Range<usize>>,
blocks_text: SharedString,
palette: Option<Palette>,
read_only: bool,
focus_handle: FocusHandle,
scroll: ScrollHandle,
_subscriptions: Vec<Subscription>,
}
impl EventEmitter<RenderedEvent> for RenderedView {}
impl RenderedView {
pub fn new(source: Entity<EditorState>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let subscriptions = vec![
cx.observe(&source, |_, _, cx| cx.notify()),
cx.subscribe_in(
&source,
window,
|this, _, event: &InputEvent, window, cx| {
if let InputEvent::Change = event
&& !this.active_matches_document(cx)
{
this.deactivate(window, cx);
}
},
),
];
Self {
source,
active: None,
blocks: Vec::new(),
blocks_text: SharedString::default(),
palette: None,
read_only: false,
focus_handle: cx.focus_handle(),
scroll: ScrollHandle::new(),
_subscriptions: subscriptions,
}
}
pub fn active_editor(&self) -> Option<Entity<TextareaState>> {
self.active.as_ref().map(|active| active.editor.clone())
}
pub fn set_palette(&mut self, palette: Option<Palette>, cx: &mut Context<Self>) {
if self.palette != palette {
self.palette = palette;
cx.notify();
}
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
#[cfg(test)]
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn set_read_only(&mut self, read_only: bool, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only == read_only {
return;
}
if read_only {
self.deactivate(window, cx);
}
self.read_only = read_only;
cx.notify();
}
fn text(&self, cx: &App) -> SharedString {
self.source.read(cx).value()
}
fn blocks(&mut self, cx: &App) -> &[Range<usize>] {
let text = self.text(cx);
if self.blocks_text != text {
self.blocks = highlight::block_ranges(&text);
self.blocks_text = text;
}
&self.blocks
}
#[cfg(test)]
pub fn block_ranges(&self) -> Vec<Range<usize>> {
self.blocks.clone()
}
fn active_range(&self) -> Option<Range<usize>> {
self.active
.as_ref()
.map(|active| active.start..active.start + active.committed_len)
}
fn active_matches_document(&self, cx: &App) -> bool {
let Some(active) = self.active.as_ref() else {
return true;
};
let range = active.start..active.start + active.committed_len;
let document = self.text(cx);
document.get(range) == Some(active.editor.read(cx).value().as_ref())
}
pub fn activate(&mut self, range: Range<usize>, window: &mut Window, cx: &mut Context<Self>) {
if self.read_only || self.active_range() == Some(range.clone()) {
return;
}
self.deactivate(window, cx);
let text = self.text(cx);
let block = text.get(range.clone()).unwrap_or_default().to_string();
let editor = cx.new(|cx| {
TextareaState::new(window, cx)
.default_value(block.clone())
.auto_grow(1, usize::MAX)
.soft_wrap(true)
.placeholder("Type here. / inserts a block.")
});
editor.update(cx, |state, cx| {
let end = state.text().len();
state.set_selected_range(end..end, cx);
state.focus(window, cx);
});
let subscriptions = vec![
cx.subscribe_in(
&editor,
window,
|this, editor, event: &InputEvent, window, cx| {
if let InputEvent::Change = event {
this.commit(window, cx);
let state = editor.read(cx);
if insert::cursor_line_trimmed(state) == "/"
&& state.cursor() == insert::cursor_line_range(state).end
{
cx.emit(RenderedEvent::SlashTyped);
}
cx.notify();
}
},
),
cx.observe(&editor, |_, _, cx| cx.notify()),
];
self.active = Some(ActiveBlock {
start: range.start,
committed_len: range.len(),
editor,
_subscriptions: subscriptions,
});
cx.notify();
}
pub fn activate_at_end(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let end = self.text(cx).len();
self.activate(end..end, window, cx);
}
pub fn deactivate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.active.take().is_some() {
self.focus_handle.focus(window, cx);
cx.notify();
}
}
fn commit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(active) = self.active.as_mut() else {
return;
};
let typed = active.editor.read(cx).value();
let range = active.start..active.start + active.committed_len;
let document = self.source.read(cx).value();
let before = &document[..range.start];
let needs_gap =
!before.is_empty() && !before.ends_with("\n\n") && !before.ends_with("\r\n\r\n");
let text = if needs_gap && !typed.is_empty() && active.committed_len == 0 {
let newline = if before.contains("\r\n") {
"\r\n"
} else {
"\n"
};
let missing = if before.ends_with(newline) { 1 } else { 2 };
format!("{}{}", newline.repeat(missing), typed)
} else {
typed.to_string()
};
active.start += text.len() - typed.len();
active.committed_len = typed.len();
let start = active.start;
self.source.update(cx, |state, cx| {
state.set_selected_range(range.clone(), cx);
state.replace(&text, window, cx);
});
let growth = text.len() as isize - range.len() as isize;
let moved = |offset: usize| (offset as isize + growth) as usize;
let inside = highlight::block_ranges(&typed)
.into_iter()
.map(|block| block.start + start..block.end + start);
let before = self
.blocks
.iter()
.take_while(|block| block.end <= range.start)
.cloned();
let after = self
.blocks
.iter()
.filter(|block| block.start >= range.end && block.end > range.start)
.map(|block| moved(block.start)..moved(block.end));
self.blocks = before.chain(inside).chain(after).collect();
self.blocks_text = self.text(cx);
}
}
impl Render for RenderedView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ranges = self.blocks(cx).to_vec();
let text = self.blocks_text.clone();
let active = self.active_range();
let palette = self.palette;
let background = palette.map_or(cx.theme().background, |p| p.background);
let foreground = palette.map_or(cx.theme().foreground, |p| p.foreground);
let muted = cx.theme().muted_foreground;
let mut items: Vec<AnyElement> = Vec::with_capacity(ranges.len() + 1);
let mut editor_placed = false;
for (ix, range) in ranges.iter().enumerate() {
if let Some(active) = &active {
let overlaps = range.start < active.end && range.end > active.start;
if overlaps {
if !editor_placed {
items.push(self.render_active(cx));
editor_placed = true;
}
continue;
}
if !editor_placed && range.start >= active.end {
items.push(self.render_active(cx));
editor_placed = true;
}
}
items.push(self.render_block(ix, range.clone(), &text, palette, cx));
}
if active.is_some() && !editor_placed {
items.push(self.render_active(cx));
}
if items.is_empty() {
let hint = if self.read_only {
"Nothing to read yet. Edit (the button in the title bar) to start writing."
} else {
"Click here to start writing."
};
items.push(
div()
.id("empty")
.text_color(muted)
.child(hint)
.into_any_element(),
);
}
v_flex()
.id("rendered")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll)
.track_focus(&self.focus_handle)
.bg(background)
.text_color(foreground)
.when_some(palette.and_then(|p| p.font_family()), |this, family| {
this.font_family(family)
})
.px_6()
.py_4()
.gap_3()
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
this.activate_at_end(window, cx);
window.prevent_default();
}),
)
.on_action(cx.listener(|this, _: &Escape, window, cx| this.deactivate(window, cx)))
.children(items)
}
}
impl RenderedView {
fn render_block(
&self,
ix: usize,
range: Range<usize>,
text: &str,
palette: Option<Palette>,
cx: &mut Context<Self>,
) -> AnyElement {
let source: SharedString = text[range.clone()].to_string().into();
div()
.id(("block", ix))
.w_full()
.when(!self.read_only, |this| this.cursor_text())
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, window, cx| {
cx.stop_propagation();
this.activate(range.clone(), window, cx);
}),
)
.child(
TextView::markdown(("block-text", ix), source)
.when_some(palette, |view, p| view.style(p.text_view_style())),
)
.into_any_element()
}
fn render_active(&self, _cx: &mut Context<Self>) -> AnyElement {
let Some(active) = self.active.as_ref() else {
return div().into_any_element();
};
div()
.id("active-block")
.w_full()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
Textarea::new(&active.editor)
.bordered(false)
.appearance(false),
)
.into_any_element()
}
}