markdown preview: Allow toggling checkbox by click (#10364)
Release Notes: - Added support for toggling a checkbox in markdown preview by clicking on it (cmd+click) ([#5226](https://github.com/zed-industries/zed/issues/5226)). --------- Co-authored-by: Remco Smits <62463826+RemcoSmitsDev@users.noreply.github.com>
This commit is contained in:
parent
eb6f7c1240
commit
fef0516f5b
4 changed files with 117 additions and 37 deletions
|
@ -58,7 +58,7 @@ pub struct ParsedMarkdownListItem {
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
pub enum ParsedMarkdownListItemType {
|
pub enum ParsedMarkdownListItemType {
|
||||||
Ordered(u64),
|
Ordered(u64),
|
||||||
Task(bool),
|
Task(bool, Range<usize>),
|
||||||
Unordered,
|
Unordered,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -502,8 +502,8 @@ impl<'a> MarkdownParser<'a> {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Event::TaskListMarker(checked)) = self.current_event() {
|
if let Some((Event::TaskListMarker(checked), range)) = self.current() {
|
||||||
task_item = Some(*checked);
|
task_item = Some((*checked, range.clone()));
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -531,8 +531,8 @@ impl<'a> MarkdownParser<'a> {
|
||||||
Event::End(TagEnd::Item) => {
|
Event::End(TagEnd::Item) => {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
|
||||||
let item_type = if let Some(checked) = task_item {
|
let item_type = if let Some((checked, range)) = task_item {
|
||||||
ParsedMarkdownListItemType::Task(checked)
|
ParsedMarkdownListItemType::Task(checked, range)
|
||||||
} else if let Some(order) = order {
|
} else if let Some(order) = order {
|
||||||
ParsedMarkdownListItemType::Ordered(order)
|
ParsedMarkdownListItemType::Ordered(order)
|
||||||
} else {
|
} else {
|
||||||
|
@ -906,8 +906,8 @@ Some other content
|
||||||
parsed.children,
|
parsed.children,
|
||||||
vec![list(
|
vec![list(
|
||||||
vec![
|
vec![
|
||||||
list_item(1, Task(false), vec![p("TODO", 2..5)]),
|
list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]),
|
||||||
list_item(1, Task(true), vec![p("Checked", 13..16)]),
|
list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]),
|
||||||
],
|
],
|
||||||
0..25
|
0..25
|
||||||
),]
|
),]
|
||||||
|
@ -929,8 +929,8 @@ Some other content
|
||||||
parsed.children,
|
parsed.children,
|
||||||
vec![list(
|
vec![list(
|
||||||
vec![
|
vec![
|
||||||
list_item(1, Task(false), vec![p("Task 1", 2..5)]),
|
list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]),
|
||||||
list_item(1, Task(true), vec![p("Task 2", 16..19)]),
|
list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]),
|
||||||
],
|
],
|
||||||
0..27
|
0..27
|
||||||
),]
|
),]
|
||||||
|
|
|
@ -144,12 +144,39 @@ impl MarkdownPreviewView {
|
||||||
let list_state =
|
let list_state =
|
||||||
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
|
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
|
||||||
if let Some(view) = view.upgrade() {
|
if let Some(view) = view.upgrade() {
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |this, cx| {
|
||||||
let Some(contents) = &view.contents else {
|
let Some(contents) = &this.contents else {
|
||||||
return div().into_any();
|
return div().into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut render_cx =
|
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 block = contents.children.get(ix).unwrap();
|
||||||
let rendered_block = render_markdown_block(block, &mut render_cx);
|
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()
|
let indicator = div()
|
||||||
.h_full()
|
.h_full()
|
||||||
.w(px(4.0))
|
.w(px(4.0))
|
||||||
.when(ix == view.selected_block, |this| {
|
.when(ix == this.selected_block, |this| {
|
||||||
this.bg(cx.theme().colors().border)
|
this.bg(cx.theme().colors().border)
|
||||||
})
|
})
|
||||||
.group_hover("markdown-block", |s| {
|
.group_hover("markdown-block", |s| {
|
||||||
if ix == view.selected_block {
|
if ix == this.selected_block {
|
||||||
s
|
s
|
||||||
} else {
|
} else {
|
||||||
s.bg(cx.theme().colors().border_variant)
|
s.bg(cx.theme().colors().border_variant)
|
||||||
|
@ -183,7 +210,7 @@ impl MarkdownPreviewView {
|
||||||
})
|
})
|
||||||
.rounded_sm();
|
.rounded_sm();
|
||||||
|
|
||||||
this.child(
|
container.child(
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.child(div().pl_4().child(rendered_block))
|
.child(div().pl_4().child(rendered_block))
|
||||||
|
@ -262,7 +289,7 @@ impl MarkdownPreviewView {
|
||||||
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
|
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
|
||||||
match event {
|
match event {
|
||||||
EditorEvent::Edited => {
|
EditorEvent::Edited => {
|
||||||
this.on_editor_edited(cx);
|
this.parse_markdown_from_active_editor(true, cx);
|
||||||
}
|
}
|
||||||
EditorEvent::SelectionsChanged { .. } => {
|
EditorEvent::SelectionsChanged { .. } => {
|
||||||
let editor = editor.read(cx);
|
let editor = editor.read(cx);
|
||||||
|
@ -285,16 +312,20 @@ impl MarkdownPreviewView {
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(state) = &self.active_editor {
|
self.parse_markdown_from_active_editor(false, cx);
|
||||||
self.parsing_markdown_task =
|
|
||||||
Some(self.parse_markdown_in_background(false, state.editor.clone(), 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 {
|
if let Some(state) = &self.active_editor {
|
||||||
self.parsing_markdown_task =
|
self.parsing_markdown_task = Some(self.parse_markdown_in_background(
|
||||||
Some(self.parse_markdown_in_background(true, state.editor.clone(), cx));
|
wait_for_debounce,
|
||||||
|
state.editor.clone(),
|
||||||
|
cx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,22 @@ use crate::markdown_elements::{
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
|
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
|
||||||
HighlightStyle, Hsla, InteractiveText, IntoElement, ParentElement, SharedString, Styled,
|
HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Modifiers, ParentElement,
|
||||||
StyledText, TextStyle, WeakView, WindowContext,
|
SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
ops::{Mul, Range},
|
ops::{Mul, Range},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, SyntaxTheme};
|
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;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut WindowContext)>>;
|
||||||
|
|
||||||
pub struct RenderContext {
|
pub struct RenderContext {
|
||||||
workspace: Option<WeakView<Workspace>>,
|
workspace: Option<WeakView<Workspace>>,
|
||||||
next_id: usize,
|
next_id: usize,
|
||||||
|
@ -27,6 +32,7 @@ pub struct RenderContext {
|
||||||
code_span_background_color: Hsla,
|
code_span_background_color: Hsla,
|
||||||
syntax_theme: Arc<SyntaxTheme>,
|
syntax_theme: Arc<SyntaxTheme>,
|
||||||
indent: usize,
|
indent: usize,
|
||||||
|
checkbox_clicked_callback: Option<CheckboxClickedCallback>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderContext {
|
impl RenderContext {
|
||||||
|
@ -44,9 +50,18 @@ impl RenderContext {
|
||||||
text_muted_color: theme.colors().text_muted,
|
text_muted_color: theme.colors().text_muted,
|
||||||
code_block_background_color: theme.colors().surface_background,
|
code_block_background_color: theme.colors().surface_background,
|
||||||
code_span_background_color: theme.colors().editor_document_highlight_read_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 {
|
fn next_id(&mut self, span: &Range<usize>) -> ElementId {
|
||||||
let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end);
|
let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end);
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
|
@ -138,19 +153,53 @@ fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) ->
|
||||||
for item in &parsed.children {
|
for item in &parsed.children {
|
||||||
let padding = rems((item.depth - 1) as f32 * 0.25);
|
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(),
|
Ordered(order) => format!("{}.", order).into_any_element(),
|
||||||
Unordered => "•".into_any_element(),
|
Unordered => "•".into_any_element(),
|
||||||
Task(checked) => div()
|
Task(checked, range) => div()
|
||||||
|
.id(cx.next_id(range))
|
||||||
.mt(px(3.))
|
.mt(px(3.))
|
||||||
.child(Checkbox::new(
|
.child(
|
||||||
"checkbox",
|
Checkbox::new(
|
||||||
if checked {
|
"checkbox",
|
||||||
Selection::Selected
|
if *checked {
|
||||||
} else {
|
Selection::Selected
|
||||||
Selection::Unselected
|
} 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(),
|
.into_any_element(),
|
||||||
};
|
};
|
||||||
let bullet = div().mr_2().child(bullet);
|
let bullet = div().mr_2().child(bullet);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue