mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-23 06:56:33 +00:00
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))]
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue