1use std::{ops::Range, path::PathBuf};
2
3use editor::{Editor, EditorEvent};
4use gpui::{
5 list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
6 IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
7};
8use ui::prelude::*;
9use workspace::item::{Item, ItemHandle};
10use workspace::Workspace;
11
12use crate::{
13 markdown_elements::ParsedMarkdown,
14 markdown_parser::parse_markdown,
15 markdown_renderer::{render_markdown_block, RenderContext},
16 OpenPreview,
17};
18
19pub struct MarkdownPreviewView {
20 workspace: WeakView<Workspace>,
21 focus_handle: FocusHandle,
22 contents: ParsedMarkdown,
23 selected_block: usize,
24 list_state: ListState,
25 tab_description: String,
26}
27
28impl MarkdownPreviewView {
29 pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
30 workspace.register_action(move |workspace, _: &OpenPreview, cx| {
31 if workspace.has_active_modal(cx) {
32 cx.propagate();
33 return;
34 }
35
36 if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
37 let workspace_handle = workspace.weak_handle();
38 let tab_description = editor.tab_description(0, cx);
39 let view: View<MarkdownPreviewView> =
40 MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx);
41 workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
42 cx.notify();
43 }
44 });
45 }
46
47 pub fn new(
48 active_editor: View<Editor>,
49 workspace: WeakView<Workspace>,
50 tab_description: Option<SharedString>,
51 cx: &mut ViewContext<Workspace>,
52 ) -> View<Self> {
53 cx.new_view(|cx: &mut ViewContext<Self>| {
54 let view = cx.view().downgrade();
55 let editor = active_editor.read(cx);
56
57 let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
58 let contents = editor.buffer().read(cx).snapshot(cx).text();
59 let contents = parse_markdown(&contents, file_location);
60
61 cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| {
62 match event {
63 EditorEvent::Edited => {
64 let editor = editor.read(cx);
65 let contents = editor.buffer().read(cx).snapshot(cx).text();
66 let file_location =
67 MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
68 this.contents = parse_markdown(&contents, file_location);
69 this.list_state.reset(this.contents.children.len());
70 cx.notify();
71
72 // TODO: This does not work as expected.
73 // The scroll request appears to be dropped
74 // after `.reset` is called.
75 this.list_state.scroll_to_reveal_item(this.selected_block);
76 cx.notify();
77 }
78 EditorEvent::SelectionsChanged { .. } => {
79 let editor = editor.read(cx);
80 let selection_range = editor.selections.last::<usize>(cx).range();
81 this.selected_block = this.get_block_index_under_cursor(selection_range);
82 this.list_state.scroll_to_reveal_item(this.selected_block);
83 cx.notify();
84 }
85 _ => {}
86 };
87 })
88 .detach();
89
90 let list_state = ListState::new(
91 contents.children.len(),
92 gpui::ListAlignment::Top,
93 px(1000.),
94 move |ix, cx| {
95 if let Some(view) = view.upgrade() {
96 view.update(cx, |view, cx| {
97 let mut render_cx =
98 RenderContext::new(Some(view.workspace.clone()), cx);
99 let block = view.contents.children.get(ix).unwrap();
100 let block = render_markdown_block(block, &mut render_cx);
101 let block = div().child(block).pl_4().pb_3();
102
103 if ix == view.selected_block {
104 let indicator = div()
105 .h_full()
106 .w(px(4.0))
107 .bg(cx.theme().colors().border)
108 .rounded_sm();
109
110 return div()
111 .relative()
112 .child(block)
113 .child(indicator.absolute().left_0().top_0())
114 .into_any();
115 }
116
117 block.into_any()
118 })
119 } else {
120 div().into_any()
121 }
122 },
123 );
124
125 let tab_description = tab_description
126 .map(|tab_description| format!("Preview {}", tab_description))
127 .unwrap_or("Markdown preview".to_string());
128
129 Self {
130 selected_block: 0,
131 focus_handle: cx.focus_handle(),
132 workspace,
133 contents,
134 list_state,
135 tab_description: tab_description,
136 }
137 })
138 }
139
140 /// The absolute path of the file that is currently being previewed.
141 fn get_folder_for_active_editor(
142 editor: &Editor,
143 cx: &ViewContext<MarkdownPreviewView>,
144 ) -> Option<PathBuf> {
145 if let Some(file) = editor.file_at(0, cx) {
146 if let Some(file) = file.as_local() {
147 file.abs_path(cx).parent().map(|p| p.to_path_buf())
148 } else {
149 None
150 }
151 } else {
152 None
153 }
154 }
155
156 fn get_block_index_under_cursor(&self, selection_range: Range<usize>) -> usize {
157 let mut block_index = 0;
158 let cursor = selection_range.start;
159
160 for (i, block) in self.contents.children.iter().enumerate() {
161 let Range { start, end } = block.source_range();
162 if start <= cursor && end >= cursor {
163 block_index = i;
164 break;
165 }
166 }
167
168 return block_index;
169 }
170}
171
172impl FocusableView for MarkdownPreviewView {
173 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
174 self.focus_handle.clone()
175 }
176}
177
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub enum PreviewEvent {}
180
181impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
182
183impl Item for MarkdownPreviewView {
184 type Event = PreviewEvent;
185
186 fn tab_content(
187 &self,
188 _detail: Option<usize>,
189 selected: bool,
190 _cx: &WindowContext,
191 ) -> AnyElement {
192 h_flex()
193 .gap_2()
194 .child(Icon::new(IconName::FileDoc).color(if selected {
195 Color::Default
196 } else {
197 Color::Muted
198 }))
199 .child(
200 Label::new(self.tab_description.to_string()).color(if selected {
201 Color::Default
202 } else {
203 Color::Muted
204 }),
205 )
206 .into_any()
207 }
208
209 fn telemetry_event_text(&self) -> Option<&'static str> {
210 Some("markdown preview")
211 }
212
213 fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
214}
215
216impl Render for MarkdownPreviewView {
217 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
218 v_flex()
219 .id("MarkdownPreview")
220 .key_context("MarkdownPreview")
221 .track_focus(&self.focus_handle)
222 .size_full()
223 .bg(cx.theme().colors().editor_background)
224 .p_4()
225 .child(
226 div()
227 .flex_grow()
228 .map(|this| this.child(list(self.list_state.clone()).size_full())),
229 )
230 }
231}