markdown preview: highlight code blocks (#9087)

![image](https://github.com/zed-industries/zed/assets/53836821/e20acd87-9680-4e1c-818d-7ae900bf0e31)

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:
Bennet Bo Fenner 2024-03-12 11:54:12 +01:00 committed by GitHub
parent e5bd9f184b
commit d362588055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 264 additions and 126 deletions

View file

@ -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()
}
}