markdown preview: highlight code blocks (#9087)
 Release Notes: - Added syntax highlighting to code blocks in markdown preview - Fixed scroll position in markdown preview when editing a markdown file (#9208)
This commit is contained in:
parent
e5bd9f184b
commit
d362588055
9 changed files with 264 additions and 126 deletions
|
@ -1,3 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use editor::{Editor, EditorEvent};
|
||||
|
@ -5,6 +6,7 @@ use gpui::{
|
|||
list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
||||
IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use ui::prelude::*;
|
||||
use workspace::item::{Item, ItemHandle};
|
||||
use workspace::Workspace;
|
||||
|
@ -19,7 +21,7 @@ use crate::{
|
|||
pub struct MarkdownPreviewView {
|
||||
workspace: WeakView<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
contents: ParsedMarkdown,
|
||||
contents: Option<ParsedMarkdown>,
|
||||
selected_block: usize,
|
||||
list_state: ListState,
|
||||
tab_description: String,
|
||||
|
@ -34,10 +36,16 @@ impl MarkdownPreviewView {
|
|||
}
|
||||
|
||||
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
|
||||
let language_registry = workspace.project().read(cx).languages().clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let tab_description = editor.tab_description(0, cx);
|
||||
let view: View<MarkdownPreviewView> =
|
||||
MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx);
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
editor,
|
||||
workspace_handle,
|
||||
tab_description,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -48,55 +56,82 @@ impl MarkdownPreviewView {
|
|||
active_editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
tab_description: Option<SharedString>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<Self> {
|
||||
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
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 contents = parse_markdown(&contents, file_location);
|
||||
|
||||
cx.subscribe(&active_editor, |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);
|
||||
this.contents = parse_markdown(&contents, file_location);
|
||||
this.list_state.reset(this.contents.children.len());
|
||||
cx.notify();
|
||||
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;
|
||||
|
||||
// TODO: This does not work as expected.
|
||||
// The scroll request appears to be dropped
|
||||
// after `.reset` is called.
|
||||
this.list_state.scroll_to_reveal_item(this.selected_block);
|
||||
cx.notify();
|
||||
}
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
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();
|
||||
|
||||
let list_state = ListState::new(
|
||||
contents.children.len(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, cx| {
|
||||
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 =
|
||||
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 {
|
||||
return div().into_any();
|
||||
};
|
||||
let mut render_cx =
|
||||
RenderContext::new(Some(view.workspace.clone()), cx);
|
||||
let block = view.contents.children.get(ix).unwrap();
|
||||
let block = contents.children.get(ix).unwrap();
|
||||
let block = render_markdown_block(block, &mut render_cx);
|
||||
let block = div().child(block).pl_4().pb_3();
|
||||
|
||||
|
@ -119,8 +154,7 @@ impl MarkdownPreviewView {
|
|||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let tab_description = tab_description
|
||||
.map(|tab_description| format!("Preview {}", tab_description))
|
||||
|
@ -130,9 +164,9 @@ impl MarkdownPreviewView {
|
|||
selected_block: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
workspace,
|
||||
contents,
|
||||
contents: None,
|
||||
list_state,
|
||||
tab_description: tab_description,
|
||||
tab_description,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -154,18 +188,33 @@ impl MarkdownPreviewView {
|
|||
}
|
||||
|
||||
fn get_block_index_under_cursor(&self, selection_range: Range<usize>) -> usize {
|
||||
let mut block_index = 0;
|
||||
let mut block_index = None;
|
||||
let cursor = selection_range.start;
|
||||
|
||||
for (i, block) in self.contents.children.iter().enumerate() {
|
||||
let Range { start, end } = block.source_range();
|
||||
if start <= cursor && end >= cursor {
|
||||
block_index = i;
|
||||
break;
|
||||
let mut last_end = 0;
|
||||
if let Some(content) = &self.contents {
|
||||
for (i, block) in content.children.iter().enumerate() {
|
||||
let Range { start, end } = block.source_range();
|
||||
|
||||
// Check if the cursor is between the last block and the current block
|
||||
if last_end > cursor && cursor < start {
|
||||
block_index = Some(i.saturating_sub(1));
|
||||
break;
|
||||
}
|
||||
|
||||
if start <= cursor && end >= cursor {
|
||||
block_index = Some(i);
|
||||
break;
|
||||
}
|
||||
last_end = end;
|
||||
}
|
||||
|
||||
if block_index.is_none() && last_end < cursor {
|
||||
block_index = Some(content.children.len().saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
return block_index;
|
||||
block_index.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue