Detailed changes
@@ -58,7 +58,7 @@ pub struct ParsedMarkdownListItem {
#[cfg_attr(test, derive(PartialEq))]
pub enum ParsedMarkdownListItemType {
Ordered(u64),
- Task(bool),
+ Task(bool, Range<usize>),
Unordered,
}
@@ -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
),]
@@ -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<Self>) {
+ fn parse_markdown_from_active_editor(
+ &mut self,
+ wait_for_debounce: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
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,
+ ));
}
}
@@ -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<Box<dyn Fn(bool, Range<usize>, &mut WindowContext)>>;
+
pub struct RenderContext {
workspace: Option<WeakView<Workspace>>,
next_id: usize,
@@ -27,6 +32,7 @@ pub struct RenderContext {
code_span_background_color: Hsla,
syntax_theme: Arc<SyntaxTheme>,
indent: usize,
+ checkbox_clicked_callback: Option<CheckboxClickedCallback>,
}
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<usize>, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback)));
+ self
+ }
+
fn next_id(&mut self, span: &Range<usize>) -> 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);