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