diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 77ec160926..75dc0bcbf7 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -58,7 +58,7 @@ pub struct ParsedMarkdownListItem { #[cfg_attr(test, derive(PartialEq))] pub enum ParsedMarkdownListItemType { Ordered(u64), - Task(bool), + Task(bool, Range), Unordered, } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 2ebc740604..fc37d3e3f5 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -502,8 +502,8 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; } - if let Some(Event::TaskListMarker(checked)) = self.current_event() { - task_item = Some(*checked); + if let Some((Event::TaskListMarker(checked), range)) = self.current() { + task_item = Some((*checked, range.clone())); self.cursor += 1; } } @@ -531,8 +531,8 @@ impl<'a> MarkdownParser<'a> { Event::End(TagEnd::Item) => { self.cursor += 1; - let item_type = if let Some(checked) = task_item { - ParsedMarkdownListItemType::Task(checked) + let item_type = if let Some((checked, range)) = task_item { + ParsedMarkdownListItemType::Task(checked, range) } else if let Some(order) = order { ParsedMarkdownListItemType::Ordered(order) } else { @@ -906,8 +906,8 @@ Some other content parsed.children, vec![list( vec![ - list_item(1, Task(false), vec![p("TODO", 2..5)]), - list_item(1, Task(true), vec![p("Checked", 13..16)]), + list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]), + list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]), ], 0..25 ),] @@ -929,8 +929,8 @@ Some other content parsed.children, vec![list( vec![ - list_item(1, Task(false), vec![p("Task 1", 2..5)]), - list_item(1, Task(true), vec![p("Task 2", 16..19)]), + list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]), + list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]), ], 0..27 ),] diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index faf11cae9b..7c9ecbfd26 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -144,12 +144,39 @@ impl MarkdownPreviewView { let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| { - let Some(contents) = &view.contents else { + view.update(cx, |this, cx| { + let Some(contents) = &this.contents else { return div().into_any(); }; + let mut render_cx = - RenderContext::new(Some(view.workspace.clone()), cx); + RenderContext::new(Some(this.workspace.clone()), cx) + .with_checkbox_clicked_callback({ + let view = view.clone(); + move |checked, source_range, cx| { + view.update(cx, |view, cx| { + if let Some(editor) = view + .active_editor + .as_ref() + .map(|s| s.editor.clone()) + { + editor.update(cx, |editor, cx| { + let task_marker = + if checked { "[x]" } else { "[ ]" }; + + editor.edit( + vec![(source_range, task_marker)], + cx, + ); + }); + view.parse_markdown_from_active_editor( + false, cx, + ); + cx.notify(); + } + }) + } + }); let block = contents.children.get(ix).unwrap(); let rendered_block = render_markdown_block(block, &mut render_cx); @@ -167,15 +194,15 @@ impl MarkdownPreviewView { } } })) - .map(move |this| { + .map(move |container| { let indicator = div() .h_full() .w(px(4.0)) - .when(ix == view.selected_block, |this| { + .when(ix == this.selected_block, |this| { this.bg(cx.theme().colors().border) }) .group_hover("markdown-block", |s| { - if ix == view.selected_block { + if ix == this.selected_block { s } else { s.bg(cx.theme().colors().border_variant) @@ -183,7 +210,7 @@ impl MarkdownPreviewView { }) .rounded_sm(); - this.child( + container.child( div() .relative() .child(div().pl_4().child(rendered_block)) @@ -262,7 +289,7 @@ impl MarkdownPreviewView { let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| { match event { EditorEvent::Edited => { - this.on_editor_edited(cx); + this.parse_markdown_from_active_editor(true, cx); } EditorEvent::SelectionsChanged { .. } => { let editor = editor.read(cx); @@ -285,16 +312,20 @@ impl MarkdownPreviewView { _subscription: subscription, }); - if let Some(state) = &self.active_editor { - self.parsing_markdown_task = - Some(self.parse_markdown_in_background(false, state.editor.clone(), cx)); - } + self.parse_markdown_from_active_editor(false, cx); } - fn on_editor_edited(&mut self, cx: &mut ViewContext) { + fn parse_markdown_from_active_editor( + &mut self, + wait_for_debounce: bool, + cx: &mut ViewContext, + ) { if let Some(state) = &self.active_editor { - self.parsing_markdown_task = - Some(self.parse_markdown_in_background(true, state.editor.clone(), cx)); + self.parsing_markdown_task = Some(self.parse_markdown_in_background( + wait_for_debounce, + state.editor.clone(), + cx, + )); } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 6903481152..da86e772e5 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -5,17 +5,22 @@ use crate::markdown_elements::{ }; use gpui::{ div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId, - HighlightStyle, Hsla, InteractiveText, IntoElement, ParentElement, SharedString, Styled, - StyledText, TextStyle, WeakView, WindowContext, + HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Modifiers, ParentElement, + SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext, }; use std::{ ops::{Mul, Range}, sync::Arc, }; use theme::{ActiveTheme, SyntaxTheme}; -use ui::{h_flex, v_flex, Checkbox, LinkPreview, Selection}; +use ui::{ + h_flex, v_flex, Checkbox, FluentBuilder, InteractiveElement, LinkPreview, Selection, + StatefulInteractiveElement, Tooltip, +}; use workspace::Workspace; +type CheckboxClickedCallback = Arc, &mut WindowContext)>>; + pub struct RenderContext { workspace: Option>, next_id: usize, @@ -27,6 +32,7 @@ pub struct RenderContext { code_span_background_color: Hsla, syntax_theme: Arc, indent: usize, + checkbox_clicked_callback: Option, } impl RenderContext { @@ -44,9 +50,18 @@ impl RenderContext { text_muted_color: theme.colors().text_muted, code_block_background_color: theme.colors().surface_background, code_span_background_color: theme.colors().editor_document_highlight_read_background, + checkbox_clicked_callback: None, } } + pub fn with_checkbox_clicked_callback( + mut self, + callback: impl Fn(bool, Range, &mut WindowContext) + 'static, + ) -> Self { + self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); + self + } + fn next_id(&mut self, span: &Range) -> ElementId { let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end); self.next_id += 1; @@ -138,19 +153,53 @@ fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> for item in &parsed.children { let padding = rems((item.depth - 1) as f32 * 0.25); - let bullet = match item.item_type { + let bullet = match &item.item_type { Ordered(order) => format!("{}.", order).into_any_element(), Unordered => "•".into_any_element(), - Task(checked) => div() + Task(checked, range) => div() + .id(cx.next_id(range)) .mt(px(3.)) - .child(Checkbox::new( - "checkbox", - if checked { - Selection::Selected - } else { - Selection::Unselected - }, - )) + .child( + Checkbox::new( + "checkbox", + if *checked { + Selection::Selected + } else { + Selection::Unselected + }, + ) + .when_some( + cx.checkbox_clicked_callback.clone(), + |this, callback| { + this.on_click({ + let range = range.clone(); + move |selection, cx| { + let checked = match selection { + Selection::Selected => true, + Selection::Unselected => false, + _ => return, + }; + + if cx.modifiers().secondary() { + callback(checked, range.clone(), cx); + } + } + }) + }, + ), + ) + .hover(|s| s.cursor_pointer()) + .tooltip(|cx| { + let secondary_modifier = Keystroke { + key: "".to_string(), + modifiers: Modifiers::secondary_key(), + ime_key: None, + }; + Tooltip::text( + format!("{}-click to toggle the checkbox", secondary_modifier), + cx, + ) + }) .into_any_element(), }; let bullet = div().mr_2().child(bullet);