markdown preview: Improve live preview (#10205)
This PR contains various improvements for the markdown preview (some of which were originally part of #7601). Some improvements can be seen in the video (see also release notes down below): https://github.com/zed-industries/zed/assets/53836821/93324ee8-d366-464a-9728-981eddbfdaf7 Release Notes: - Added action to open markdown preview in the same pane - Added support for displaying channel notes in markdown preview - Added support for displaying the current active editor when opening markdown preview - Added support for scrolling the editor to the corresponding block when double clicking an element in markdown preview - Improved pane creation handling when opening markdown preview - Fixed markdown preview displaying non-markdown files
This commit is contained in:
parent
d009d84ead
commit
7dccbd8e3b
7 changed files with 295 additions and 109 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5675,11 +5675,13 @@ dependencies = [
|
||||||
name = "markdown_preview"
|
name = "markdown_preview"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"async-recursion 1.0.5",
|
"async-recursion 1.0.5",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"linkify",
|
"linkify",
|
||||||
|
"log",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"theme",
|
"theme",
|
||||||
|
|
|
@ -11,7 +11,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use isahc::AsyncBody;
|
use isahc::AsyncBody;
|
||||||
|
|
||||||
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
|
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
|
@ -238,10 +238,11 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||||
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
||||||
let workspace_handle = workspace.weak_handle();
|
let workspace_handle = workspace.weak_handle();
|
||||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||||
|
MarkdownPreviewMode::Default,
|
||||||
editor,
|
editor,
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
Some(tab_description),
|
|
||||||
language_registry,
|
language_registry,
|
||||||
|
Some(tab_description),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
|
workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
|
||||||
|
|
|
@ -15,11 +15,13 @@ path = "src/markdown_preview.rs"
|
||||||
test-support = []
|
test-support = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
async-recursion.workspace = true
|
async-recursion.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
linkify.workspace = true
|
linkify.workspace = true
|
||||||
|
log.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
pulldown-cmark.workspace = true
|
pulldown-cmark.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
|
|
@ -270,7 +270,7 @@ impl<'a> MarkdownParser<'a> {
|
||||||
regions.push(ParsedRegion {
|
regions.push(ParsedRegion {
|
||||||
code: false,
|
code: false,
|
||||||
link: Some(Link::Web {
|
link: Some(Link::Web {
|
||||||
url: t[range].to_string(),
|
url: link.as_str().to_string(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub mod markdown_parser;
|
||||||
pub mod markdown_preview_view;
|
pub mod markdown_preview_view;
|
||||||
pub mod markdown_renderer;
|
pub mod markdown_renderer;
|
||||||
|
|
||||||
actions!(markdown, [OpenPreview]);
|
actions!(markdown, [OpenPreview, OpenPreviewToTheSide]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(|workspace: &mut Workspace, cx| {
|
cx.observe_new_views(|workspace: &mut Workspace, cx| {
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{ops::Range, path::PathBuf};
|
use std::{ops::Range, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use editor::scroll::{Autoscroll, AutoscrollStrategy};
|
||||||
use editor::{Editor, EditorEvent};
|
use editor::{Editor, EditorEvent};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
list, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle, FocusableView,
|
||||||
IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
|
InteractiveElement, IntoElement, ListState, ParentElement, Render, Styled, Subscription, Task,
|
||||||
|
View, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::item::{Item, ItemHandle};
|
use workspace::item::{Item, ItemHandle};
|
||||||
use workspace::Workspace;
|
use workspace::{Pane, Workspace};
|
||||||
|
|
||||||
|
use crate::OpenPreviewToTheSide;
|
||||||
use crate::{
|
use crate::{
|
||||||
markdown_elements::ParsedMarkdown,
|
markdown_elements::ParsedMarkdown,
|
||||||
markdown_parser::parse_markdown,
|
markdown_parser::parse_markdown,
|
||||||
|
@ -18,109 +23,123 @@ use crate::{
|
||||||
OpenPreview,
|
OpenPreview,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
pub struct MarkdownPreviewView {
|
pub struct MarkdownPreviewView {
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
|
active_editor: Option<EditorState>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
contents: Option<ParsedMarkdown>,
|
contents: Option<ParsedMarkdown>,
|
||||||
selected_block: usize,
|
selected_block: usize,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
tab_description: String,
|
tab_description: Option<String>,
|
||||||
|
fallback_tab_description: SharedString,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
parsing_markdown_task: Option<Task<Result<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum MarkdownPreviewMode {
|
||||||
|
/// The preview will always show the contents of the provided editor.
|
||||||
|
Default,
|
||||||
|
/// The preview will "follow" the currently active editor.
|
||||||
|
Follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EditorState {
|
||||||
|
editor: View<Editor>,
|
||||||
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MarkdownPreviewView {
|
impl MarkdownPreviewView {
|
||||||
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
|
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(move |workspace, _: &OpenPreview, cx| {
|
workspace.register_action(move |workspace, _: &OpenPreview, cx| {
|
||||||
if workspace.has_active_modal(cx) {
|
if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) {
|
||||||
cx.propagate();
|
let view = Self::create_markdown_view(workspace, editor, cx);
|
||||||
return;
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
if let Some(existing_view_idx) = Self::find_existing_preview_item_idx(pane) {
|
||||||
|
pane.activate_item(existing_view_idx, true, true, cx);
|
||||||
|
} else {
|
||||||
|
pane.add_item(Box::new(view.clone()), true, true, None, cx)
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
|
workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, cx| {
|
||||||
let language_registry = workspace.project().read(cx).languages().clone();
|
if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) {
|
||||||
let workspace_handle = workspace.weak_handle();
|
let view = Self::create_markdown_view(workspace, editor.clone(), cx);
|
||||||
let tab_description = editor.tab_description(0, cx);
|
let pane = workspace
|
||||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
.find_pane_in_direction(workspace::SplitDirection::Right, cx)
|
||||||
editor,
|
.unwrap_or_else(|| {
|
||||||
workspace_handle,
|
workspace.split_pane(
|
||||||
tab_description,
|
workspace.active_pane().clone(),
|
||||||
language_registry,
|
workspace::SplitDirection::Right,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
|
});
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
if let Some(existing_view_idx) = Self::find_existing_preview_item_idx(pane) {
|
||||||
|
pane.activate_item(existing_view_idx, true, true, cx);
|
||||||
|
} else {
|
||||||
|
pane.add_item(Box::new(view.clone()), false, false, None, cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
editor.focus_handle(cx).focus(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_existing_preview_item_idx(pane: &Pane) -> Option<usize> {
|
||||||
|
pane.items_of_type::<MarkdownPreviewView>()
|
||||||
|
.nth(0)
|
||||||
|
.and_then(|view| pane.index_for_item(&view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_active_item_as_markdown_editor(
|
||||||
|
workspace: &Workspace,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<View<Editor>> {
|
||||||
|
if let Some(editor) = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
{
|
||||||
|
if Self::is_markdown_file(&editor, cx) {
|
||||||
|
return Some(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_markdown_view(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
editor: View<Editor>,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> View<MarkdownPreviewView> {
|
||||||
|
let language_registry = workspace.project().read(cx).languages().clone();
|
||||||
|
let workspace_handle = workspace.weak_handle();
|
||||||
|
MarkdownPreviewView::new(
|
||||||
|
MarkdownPreviewMode::Follow,
|
||||||
|
editor,
|
||||||
|
workspace_handle,
|
||||||
|
language_registry,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
mode: MarkdownPreviewMode,
|
||||||
active_editor: View<Editor>,
|
active_editor: View<Editor>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
tab_description: Option<SharedString>,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
fallback_description: Option<SharedString>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> View<Self> {
|
) -> View<Self> {
|
||||||
cx.new_view(|cx: &mut ViewContext<Self>| {
|
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||||
let view = cx.view().downgrade();
|
let view = cx.view().downgrade();
|
||||||
let editor = active_editor.read(cx);
|
|
||||||
let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
|
|
||||||
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
|
||||||
|
|
||||||
let language_registry_copy = language_registry.clone();
|
|
||||||
cx.spawn(|view, mut cx| async move {
|
|
||||||
let contents =
|
|
||||||
parse_markdown(&contents, file_location, Some(language_registry_copy)).await;
|
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
|
||||||
let markdown_blocks_count = contents.children.len();
|
|
||||||
view.contents = Some(contents);
|
|
||||||
view.list_state.reset(markdown_blocks_count);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.subscribe(
|
|
||||||
&active_editor,
|
|
||||||
move |this, editor, event: &EditorEvent, cx| {
|
|
||||||
match event {
|
|
||||||
EditorEvent::Edited => {
|
|
||||||
let editor = editor.read(cx);
|
|
||||||
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
|
||||||
let file_location =
|
|
||||||
MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
|
|
||||||
let language_registry = language_registry.clone();
|
|
||||||
cx.spawn(move |view, mut cx| async move {
|
|
||||||
let contents = parse_markdown(
|
|
||||||
&contents,
|
|
||||||
file_location,
|
|
||||||
Some(language_registry.clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
view.update(&mut cx, move |view, cx| {
|
|
||||||
let markdown_blocks_count = contents.children.len();
|
|
||||||
view.contents = Some(contents);
|
|
||||||
|
|
||||||
let scroll_top = view.list_state.logical_scroll_top();
|
|
||||||
view.list_state.reset(markdown_blocks_count);
|
|
||||||
view.list_state.scroll_to(scroll_top);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
EditorEvent::SelectionsChanged { .. } => {
|
|
||||||
let editor = editor.read(cx);
|
|
||||||
let selection_range = editor.selections.last::<usize>(cx).range();
|
|
||||||
this.selected_block =
|
|
||||||
this.get_block_index_under_cursor(selection_range);
|
|
||||||
this.list_state.scroll_to_reveal_item(this.selected_block);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
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| {
|
||||||
|
@ -132,43 +151,200 @@ impl MarkdownPreviewView {
|
||||||
let mut render_cx =
|
let mut render_cx =
|
||||||
RenderContext::new(Some(view.workspace.clone()), cx);
|
RenderContext::new(Some(view.workspace.clone()), cx);
|
||||||
let block = contents.children.get(ix).unwrap();
|
let block = contents.children.get(ix).unwrap();
|
||||||
let block = render_markdown_block(block, &mut render_cx);
|
let rendered_block = render_markdown_block(block, &mut render_cx);
|
||||||
let block = div().child(block).pl_4().pb_3();
|
|
||||||
|
|
||||||
if ix == view.selected_block {
|
div()
|
||||||
|
.id(ix)
|
||||||
|
.pb_3()
|
||||||
|
.group("markdown-block")
|
||||||
|
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
|
||||||
|
if event.down.click_count == 2 {
|
||||||
|
if let Some(block) =
|
||||||
|
this.contents.as_ref().and_then(|c| c.children.get(ix))
|
||||||
|
{
|
||||||
|
let start = block.source_range().start;
|
||||||
|
this.move_cursor_to_block(cx, start..start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.map(move |this| {
|
||||||
let indicator = div()
|
let indicator = div()
|
||||||
.h_full()
|
.h_full()
|
||||||
.w(px(4.0))
|
.w(px(4.0))
|
||||||
.bg(cx.theme().colors().border)
|
.when(ix == view.selected_block, |this| {
|
||||||
|
this.bg(cx.theme().colors().border)
|
||||||
|
})
|
||||||
|
.group_hover("markdown-block", |s| {
|
||||||
|
if ix != view.selected_block {
|
||||||
|
s.bg(cx.theme().colors().border_variant)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
})
|
||||||
.rounded_sm();
|
.rounded_sm();
|
||||||
|
|
||||||
return div()
|
this.child(
|
||||||
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.child(block)
|
.child(div().pl_4().child(rendered_block))
|
||||||
.child(indicator.absolute().left_0().top_0())
|
.child(indicator.absolute().left_0().top_0()),
|
||||||
.into_any();
|
)
|
||||||
}
|
})
|
||||||
|
.into_any()
|
||||||
block.into_any()
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
div().into_any()
|
div().into_any()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let tab_description = tab_description
|
let mut this = Self {
|
||||||
.map(|tab_description| format!("Preview {}", tab_description))
|
|
||||||
.unwrap_or("Markdown preview".to_string());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
selected_block: 0,
|
selected_block: 0,
|
||||||
|
active_editor: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
workspace,
|
workspace: workspace.clone(),
|
||||||
contents: None,
|
contents: None,
|
||||||
list_state,
|
list_state,
|
||||||
tab_description,
|
tab_description: None,
|
||||||
}
|
language_registry,
|
||||||
|
fallback_tab_description: fallback_description
|
||||||
|
.unwrap_or_else(|| "Markdown Preview".into()),
|
||||||
|
parsing_markdown_task: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.set_editor(active_editor, cx);
|
||||||
|
|
||||||
|
if mode == MarkdownPreviewMode::Follow {
|
||||||
|
if let Some(workspace) = &workspace.upgrade() {
|
||||||
|
cx.observe(workspace, |this, workspace, cx| {
|
||||||
|
let item = workspace.read(cx).active_item(cx);
|
||||||
|
this.workspace_updated(item, cx);
|
||||||
})
|
})
|
||||||
|
.detach();
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to listen to workspace updates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_updated(
|
||||||
|
&mut self,
|
||||||
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(item) = active_item {
|
||||||
|
if item.item_id() != cx.entity_id() {
|
||||||
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||||
|
if Self::is_markdown_file(&editor, cx) {
|
||||||
|
self.set_editor(editor, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_markdown_file<V>(editor: &View<Editor>, cx: &mut ViewContext<V>) -> bool {
|
||||||
|
let language = editor.read(cx).buffer().read(cx).language_at(0, cx);
|
||||||
|
language
|
||||||
|
.map(|l| l.name().as_ref() == "Markdown")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(active) = &self.active_editor {
|
||||||
|
if active.editor == editor {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
|
||||||
|
match event {
|
||||||
|
EditorEvent::Edited => {
|
||||||
|
this.on_editor_edited(cx);
|
||||||
|
}
|
||||||
|
EditorEvent::SelectionsChanged { .. } => {
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
let selection_range = editor.selections.last::<usize>(cx).range();
|
||||||
|
this.selected_block = this.get_block_index_under_cursor(selection_range);
|
||||||
|
this.list_state.scroll_to_reveal_item(this.selected_block);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tab_description = editor
|
||||||
|
.read(cx)
|
||||||
|
.tab_description(0, cx)
|
||||||
|
.map(|tab_description| format!("Preview {}", tab_description));
|
||||||
|
|
||||||
|
self.active_editor = Some(EditorState {
|
||||||
|
editor,
|
||||||
|
_subscription: subscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(state) = &self.active_editor {
|
||||||
|
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>) {
|
||||||
|
if let Some(state) = &self.active_editor {
|
||||||
|
self.parsing_markdown_task =
|
||||||
|
Some(self.parse_markdown_in_background(true, state.editor.clone(), cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_markdown_in_background(
|
||||||
|
&mut self,
|
||||||
|
wait_for_debounce: bool,
|
||||||
|
editor: View<Editor>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let language_registry = self.language_registry.clone();
|
||||||
|
|
||||||
|
cx.spawn(move |view, mut cx| async move {
|
||||||
|
if wait_for_debounce {
|
||||||
|
// Wait for the user to stop typing
|
||||||
|
cx.background_executor().timer(REPARSE_DEBOUNCE).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (contents, file_location) = view.update(&mut cx, |_, cx| {
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
||||||
|
let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
|
||||||
|
(contents, file_location)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let parsing_task = cx.background_executor().spawn(async move {
|
||||||
|
parse_markdown(&contents, file_location, Some(language_registry)).await
|
||||||
|
});
|
||||||
|
let contents = parsing_task.await;
|
||||||
|
view.update(&mut cx, move |view, cx| {
|
||||||
|
let markdown_blocks_count = contents.children.len();
|
||||||
|
view.contents = Some(contents);
|
||||||
|
let scroll_top = view.list_state.logical_scroll_top();
|
||||||
|
view.list_state.reset(markdown_blocks_count);
|
||||||
|
view.list_state.scroll_to(scroll_top);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_to_block(&self, cx: &mut ViewContext<Self>, selection: Range<usize>) {
|
||||||
|
if let Some(state) = &self.active_editor {
|
||||||
|
state.editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(
|
||||||
|
Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
|
||||||
|
cx,
|
||||||
|
|selections| selections.select_ranges(vec![selection]),
|
||||||
|
);
|
||||||
|
editor.focus(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The absolute path of the file that is currently being previewed.
|
/// The absolute path of the file that is currently being previewed.
|
||||||
|
@ -246,7 +422,12 @@ impl Item for MarkdownPreviewView {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
Label::new(self.tab_description.to_string()).color(if selected {
|
Label::new(if let Some(description) = &self.tab_description {
|
||||||
|
description.clone().into()
|
||||||
|
} else {
|
||||||
|
self.fallback_tab_description.clone()
|
||||||
|
})
|
||||||
|
.color(if selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -2267,7 +2267,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_pane_in_direction(
|
pub fn find_pane_in_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
direction: SplitDirection,
|
direction: SplitDirection,
|
||||||
cx: &WindowContext,
|
cx: &WindowContext,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue