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