1use gpui::prelude::*;
25use gpui::{
26 canvas, div, point, px, App, Bounds, ClipboardItem, Context, Div, DragMoveEvent, Empty, Entity,
27 EntityId, EventEmitter, FocusHandle, Font, FontStyle, FontWeight, Hsla, IntoElement,
28 KeyDownEvent, MouseButton, MouseDownEvent, Pixels, ScrollHandle, SharedString,
29 StrikethroughStyle, TextAlign, TextRun, UnderlineStyle, Window, WrappedLine,
30};
31
32use super::block::{classify, Block, DocState};
33use super::layout::{
34 byte_for_col, col_for_byte, metrics, plan, src_for_vis, vis_for_src, RowKind, RowPlan,
35};
36use crate::editor::{token_color, EditorModel, Highlighter, Language, LineState, Pos, TokenKind};
37use crate::reactive::Signal;
38use crate::theme::theme;
39use crate::{Glyph, IconName};
40
41use crate::devtools::Probed;
42use crate::style::MONO_FAMILY;
44const PAD_X: f32 = 16.0;
46const PAD_Y: f32 = 12.0;
48const DEFAULT_WRAP: f32 = 640.0;
50
51#[derive(Debug, Clone)]
53pub enum MarkdownEditorEvent {
54 Change(String),
56 LinkClick(String),
59}
60
61struct MarkdownDrag(EntityId);
64
65#[derive(Clone, Copy, Default)]
68pub struct MarkdownStyle {
69 pub bare: bool,
71 pub bg: Option<Hsla>,
72 pub text: Option<Hsla>,
73 pub caret: Option<Hsla>,
74 pub selection: Option<Hsla>,
75 pub accent: Option<Hsla>,
77 pub code_bg: Option<Hsla>,
78 pub placeholder: Option<Hsla>,
79}
80
81struct Row {
84 plan: RowPlan,
85 text: Option<std::rc::Rc<WrappedLine>>,
87 line_h: f32,
88 pad_top: f32,
89 inset: f32,
90 height: f32,
91 y: f32,
92}
93
94impl Row {
95 fn visual_rows(&self) -> usize {
96 self
97 .text
98 .as_ref()
99 .map_or(1, |t| t.wrap_boundaries().len() + 1)
100 }
101
102 fn boundaries(&self) -> Vec<usize> {
104 let Some(text) = &self.text else {
105 return Vec::new();
106 };
107 text
108 .wrap_boundaries()
109 .iter()
110 .map(|b| text.runs()[b.run_ix].glyphs[b.glyph_ix].index)
111 .collect()
112 }
113
114 fn pos_end(&self, vis: usize) -> (f32, f32) {
117 let Some(text) = &self.text else {
118 return (0.0, 0.0);
119 };
120 match text.position_for_index(vis.min(text.len()), px(self.line_h)) {
121 Some(p) => (f32::from(p.x), f32::from(p.y)),
122 None => (0.0, 0.0),
123 }
124 }
125
126 fn caret(&self, vis: usize) -> (f32, usize) {
129 let bounds = self.boundaries();
130 let row = bounds.iter().filter(|&&b| b <= vis).count();
131 if bounds.contains(&vis) {
132 return (0.0, row);
133 }
134 let (x, y) = self.pos_end(vis);
135 (
136 x,
137 if self.line_h > 0.0 {
138 (y / self.line_h).round() as usize
139 } else {
140 0
141 },
142 )
143 }
144
145 fn sel_rects(&self, vs: usize, ve: usize, newline: bool, cell: f32) -> Vec<(f32, usize, f32)> {
148 let bounds = self.boundaries();
149 let (sr, er) = split_visual(&bounds, vs, ve);
150 let (sx, _) = if bounds.contains(&vs) {
151 (0.0, 0.0)
152 } else {
153 self.pos_end(vs)
154 };
155 let (ex, _) = self.pos_end(ve);
156 let row_end = |r: usize| -> f32 {
157 match bounds.get(r) {
158 Some(&b) => self.pos_end(b).0,
159 None => self.pos_end(usize::MAX).0,
160 }
161 };
162 let mut rects = Vec::new();
163 if sr == er {
164 rects.push((sx, sr, (ex - sx).max(0.0)));
165 } else {
166 rects.push((sx, sr, (row_end(sr) - sx).max(0.0)));
167 for r in sr + 1..er {
168 rects.push((0.0, r, row_end(r).max(0.0)));
169 }
170 rects.push((0.0, er, ex.max(0.0)));
171 }
172 if newline {
173 if let Some(last) = rects.last_mut() {
174 last.2 += cell;
175 }
176 }
177 rects.retain(|r| r.2 > 0.0);
178 rects
179 }
180}
181
182pub struct MarkdownEditor {
191 model: EditorModel,
192 placeholder: SharedString,
193 read_only: bool,
194 font_size: f32,
195 rows: Option<usize>,
196 style: MarkdownStyle,
197 focus: FocusHandle,
198 scroll: ScrollHandle,
199 text_bounds: Bounds<Pixels>,
201 wrap_w: f32,
203 cell_w: f32,
205 layout: Vec<Row>,
207 scroll_to_cursor: bool,
209 goal_x: Option<f32>,
211}
212
213impl EventEmitter<MarkdownEditorEvent> for MarkdownEditor {}
214
215impl MarkdownEditor {
216 pub fn new(cx: &mut Context<Self>) -> Self {
217 MarkdownEditor {
218 model: EditorModel::new(""),
219 placeholder: SharedString::default(),
220 read_only: false,
221 font_size: 15.0,
222 rows: None,
223 style: MarkdownStyle::default(),
224 focus: cx.focus_handle(),
225 scroll: ScrollHandle::new(),
226 text_bounds: Bounds::default(),
227 wrap_w: DEFAULT_WRAP,
228 cell_w: 15.0 * 0.55,
229 layout: Vec::new(),
230 scroll_to_cursor: false,
231 goal_x: None,
232 }
233 }
234
235 pub fn value(mut self, text: &str) -> Self {
239 self.model.set_text(text);
240 self
241 }
242
243 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
245 self.placeholder = placeholder.into();
246 self
247 }
248
249 pub fn read_only(mut self, read_only: bool) -> Self {
252 self.read_only = read_only;
253 self
254 }
255
256 pub fn font_size(mut self, size: f32) -> Self {
258 self.font_size = size;
259 self
260 }
261
262 pub fn rows(mut self, rows: usize) -> Self {
264 self.rows = Some(rows);
265 self
266 }
267
268 pub fn tab_size(mut self, n: usize) -> Self {
270 self.model.set_tab_size(n);
271 self
272 }
273
274 pub fn style(mut self, style: MarkdownStyle) -> Self {
276 self.style = style;
277 self
278 }
279
280 pub fn set_style(&mut self, style: MarkdownStyle, cx: &mut Context<Self>) {
282 self.style = style;
283 cx.notify();
284 }
285
286 pub fn text(&self) -> String {
290 self.model.text()
291 }
292
293 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
295 self.model.set_text(value);
296 cx.notify();
297 }
298
299 pub fn focus_handle(&self) -> FocusHandle {
301 self.focus.clone()
302 }
303
304 pub fn model(&self) -> &EditorModel {
307 &self.model
308 }
309
310 pub fn edit<R>(&mut self, cx: &mut Context<Self>, f: impl FnOnce(&mut EditorModel) -> R) -> R {
314 let before = self.model.text();
315 let result = f(&mut self.model);
316 let after = self.model.text();
317 if after != before {
318 cx.emit(MarkdownEditorEvent::Change(after));
319 }
320 self.scroll_to_cursor = true;
321 cx.notify();
322 result
323 }
324
325 pub fn bind(entity: &Entity<MarkdownEditor>, signal: &Signal<String>, cx: &mut App) {
329 let initial = signal.get(cx);
330 entity.update(cx, |this, cx| {
331 if this.text() != initial {
332 this.set_text(&initial, cx);
333 }
334 });
335 let sink = signal.clone();
336 cx.subscribe(entity, move |_editor, event: &MarkdownEditorEvent, cx| {
337 if let MarkdownEditorEvent::Change(text) = event {
338 sink.set_if_changed(cx, text.clone());
339 }
340 })
341 .detach();
342 let editor = entity.downgrade();
345 cx.observe(signal.entity(), move |observed, cx| {
346 let value = observed.read(cx).clone();
347 editor
348 .update(cx, |this, cx| {
349 if this.text() != value {
350 this.set_text(&value, cx);
351 }
352 })
353 .ok();
354 })
355 .detach();
356 }
357
358 pub fn toggle_task(&mut self, line: usize, cx: &mut Context<Self>) -> bool {
363 let Some(text) = self.model.line(line) else {
364 return false;
365 };
366 let Block::Task { checked, state, .. } = classify_alone(text) else {
367 return false;
368 };
369 let cursor = self.model.cursor();
370 self.edit(cx, |m| {
371 m.move_to(line, state, false);
373 m.move_to(line, state + 1, true);
374 m.insert(if checked { " " } else { "x" });
375 m.move_to(cursor.line, cursor.col, false);
376 });
377 true
378 }
379
380 fn toggle_wrap(&mut self, marker: &str, cx: &mut Context<Self>) {
383 if self.read_only {
384 return;
385 }
386 let chars = marker.chars().count();
387 if self.model.selection().is_none() {
388 self.model.select_word();
389 }
390 let Some((start, end)) = self.model.selection() else {
391 self.edit(cx, |m| {
393 m.insert(&format!("{marker}{marker}"));
394 for _ in 0..chars {
395 m.move_left(false);
396 }
397 });
398 return;
399 };
400 if start.line != end.line {
401 return;
402 }
403 let line = self.model.line(start.line).unwrap_or("");
405 let (sb, eb) = (byte_for_col(line, start.col), byte_for_col(line, end.col));
406 if line[..sb].ends_with(marker) && line[eb..].starts_with(marker) {
407 self.model.move_to(start.line, start.col - chars, false);
408 self.model.move_to(end.line, end.col + chars, true);
409 }
410 let Some(sel) = self.model.selected_text() else {
411 return;
412 };
413 let unwrap = sel.starts_with(marker) && sel.ends_with(marker) && sel.len() >= 2 * marker.len();
414 let replacement = if unwrap {
415 sel[marker.len()..sel.len() - marker.len()].to_string()
416 } else {
417 format!("{marker}{sel}{marker}")
418 };
419 self.edit(cx, |m| m.insert(&replacement));
420 }
421
422 fn insert_link(&mut self, cx: &mut Context<Self>) {
425 if self.read_only {
426 return;
427 }
428 let single_line = matches!(self.model.selection(), Some((s, e)) if s.line == e.line);
429 self.edit(cx, |m| {
430 if single_line {
431 let sel = m.selected_text().unwrap_or_default();
432 m.insert(&format!("[{sel}]()"));
433 m.move_left(false);
434 } else {
435 m.insert("[]()");
436 for _ in 0..3 {
437 m.move_left(false);
438 }
439 }
440 });
441 }
442
443 fn on_enter(&mut self, cx: &mut Context<Self>) {
445 let cursor = self.model.cursor();
446 let line = self.model.line(cursor.line).unwrap_or("").to_string();
447 let in_code = matches!(
448 self.layout.get(cursor.line).map(|r| &r.plan.kind),
449 Some(RowKind::Code { .. } | RowKind::Fence { .. } | RowKind::FrontMatter)
450 );
451 let marker = if in_code || self.model.selection().is_some() {
452 None
453 } else {
454 continuation(&line)
455 };
456 match marker {
457 Some(_) if line[prefix_end(&line)..].trim().is_empty() => {
458 let cols = line.chars().count();
461 self.edit(cx, |m| {
462 m.move_to(cursor.line, 0, false);
463 m.move_to(cursor.line, cols, true);
464 m.delete_selection();
465 });
466 }
467 Some(marker) => self.edit(cx, |m| {
468 m.newline();
469 m.insert(&marker);
470 }),
471 None => self.edit(cx, |m| m.newline()),
472 }
473 }
474
475 fn on_tab(&mut self, outdent: bool, cx: &mut Context<Self>) {
477 let cursor = self.model.cursor();
478 let line = self.model.line(cursor.line).unwrap_or("").to_string();
479 let is_item = matches!(
480 classify_alone(&line),
481 Block::Bullet { .. } | Block::Ordered { .. } | Block::Task { .. }
482 );
483 if !is_item {
484 if !outdent {
485 self.edit(cx, |m| m.tab());
486 }
487 return;
488 }
489 let n = self.model.tab_size();
490 if outdent {
491 let lead = line.chars().take_while(|&c| c == ' ').count().min(n);
492 if lead == 0 {
493 return;
494 }
495 self.edit(cx, |m| {
496 m.move_to(cursor.line, 0, false);
497 m.move_to(cursor.line, lead, true);
498 m.delete_selection();
499 m.move_to(cursor.line, cursor.col.saturating_sub(lead), false);
500 });
501 } else {
502 self.edit(cx, |m| {
503 m.move_to(cursor.line, 0, false);
504 m.insert(&" ".repeat(n));
505 m.move_to(cursor.line, cursor.col + n, false);
506 });
507 }
508 }
509
510 fn backspace_marker(&mut self, cx: &mut Context<Self>) -> bool {
513 let cursor = self.model.cursor();
514 let line = self.model.line(cursor.line).unwrap_or("").to_string();
515 let content = match classify_alone(&line) {
516 Block::Bullet { content, .. }
517 | Block::Ordered { content, .. }
518 | Block::Task { content, .. }
519 | Block::Quote { content, .. } => content,
520 _ => return false,
521 };
522 if content == 0 || cursor.col != content {
524 return false;
525 }
526 self.edit(cx, |m| {
527 m.move_to(cursor.line, 0, false);
528 m.move_to(cursor.line, content, true);
529 m.delete_selection();
530 });
531 true
532 }
533
534 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
537 let ks = &event.keystroke;
538 let m = ks.modifiers;
539 let shift = m.shift;
540 if !matches!(ks.key.as_str(), "up" | "down") {
541 self.goal_x = None;
542 }
543 match ks.key.as_str() {
544 "left" => {
545 if m.platform {
546 self.model.home(shift);
547 } else if m.alt {
548 self.model.word_left(shift);
549 } else {
550 self.model.move_left(shift);
551 }
552 self.after_move(cx);
553 }
554 "right" => {
555 if m.platform {
556 self.model.end(shift);
557 } else if m.alt {
558 self.model.word_right(shift);
559 } else {
560 self.model.move_right(shift);
561 }
562 self.after_move(cx);
563 }
564 "up" => {
565 if m.platform {
566 self.model.doc_start(shift);
567 self.after_move(cx);
568 } else {
569 self.move_visual(false, shift, cx);
570 }
571 }
572 "down" => {
573 if m.platform {
574 self.model.doc_end(shift);
575 self.after_move(cx);
576 } else {
577 self.move_visual(true, shift, cx);
578 }
579 }
580 "home" => {
581 if m.platform {
582 self.model.doc_start(shift);
583 } else {
584 self.model.home(shift);
585 }
586 self.after_move(cx);
587 }
588 "end" => {
589 if m.platform {
590 self.model.doc_end(shift);
591 } else {
592 self.model.end(shift);
593 }
594 self.after_move(cx);
595 }
596 "backspace" => {
597 if self.read_only {
598 return;
599 }
600 if self.model.selection().is_none() && !m.platform && !m.alt && self.backspace_marker(cx) {
601 cx.stop_propagation();
602 return;
603 }
604 let changed = if self.model.selection().is_some() {
605 self.model.delete_selection()
606 } else if m.platform {
607 self.model.home(true);
608 self.model.delete_selection()
609 } else if m.alt {
610 self.model.word_left(true);
611 self.model.delete_selection()
612 } else {
613 self.model.backspace()
614 };
615 if changed {
616 self.after_edit(cx);
617 } else {
618 cx.stop_propagation();
619 }
620 }
621 "delete" => {
622 if self.read_only {
623 return;
624 }
625 let changed = if self.model.selection().is_some() {
626 self.model.delete_selection()
627 } else if m.platform {
628 self.model.end(true);
629 self.model.delete_selection()
630 } else if m.alt {
631 self.model.word_right(true);
632 self.model.delete_selection()
633 } else {
634 self.model.delete()
635 };
636 if changed {
637 self.after_edit(cx);
638 } else {
639 cx.stop_propagation();
640 }
641 }
642 "enter" if m.platform => {
643 if !self.read_only && self.toggle_task(self.model.cursor().line, cx) {
644 cx.stop_propagation();
645 }
646 }
647 "enter" => {
648 if self.read_only {
649 return;
650 }
651 self.on_enter(cx);
652 cx.stop_propagation();
653 }
654 "tab" => {
655 if m.platform || self.read_only {
656 return;
657 }
658 self.on_tab(shift, cx);
659 cx.stop_propagation();
660 }
661 "escape" => {
663 if self.model.selection().is_some() {
664 self.model.clear_selection();
665 cx.notify();
666 }
667 }
668 "a" if m.platform => {
669 self.model.select_all();
670 cx.notify();
671 cx.stop_propagation();
672 }
673 "b" if m.platform => {
674 self.toggle_wrap("**", cx);
675 cx.stop_propagation();
676 }
677 "i" if m.platform => {
678 self.toggle_wrap("*", cx);
679 cx.stop_propagation();
680 }
681 "k" if m.platform => {
682 self.insert_link(cx);
683 cx.stop_propagation();
684 }
685 "c" if m.platform => {
686 if let Some(text) = self.model.copy() {
687 cx.write_to_clipboard(ClipboardItem::new_string(text));
688 }
689 cx.stop_propagation();
690 }
691 "x" if m.platform => {
692 if self.read_only {
693 if let Some(text) = self.model.copy() {
695 cx.write_to_clipboard(ClipboardItem::new_string(text));
696 }
697 } else if let Some(text) = self.model.cut() {
698 cx.write_to_clipboard(ClipboardItem::new_string(text));
699 self.after_edit(cx);
700 return;
701 }
702 cx.stop_propagation();
703 }
704 "v" if m.platform => {
705 if !self.read_only {
706 if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
707 if !text.is_empty() {
708 self.model.insert(&text);
709 self.after_edit(cx);
710 return;
711 }
712 }
713 }
714 cx.stop_propagation();
715 }
716 "z" if m.platform => {
717 if !self.read_only {
718 let changed = if m.shift {
719 self.model.redo()
720 } else {
721 self.model.undo()
722 };
723 if changed {
724 self.after_edit(cx);
725 return;
726 }
727 }
728 cx.stop_propagation();
729 }
730 _ => {
731 if !self.read_only && !m.platform && !m.control {
734 if let Some(text) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
735 self.model.insert(text);
736 self.after_edit(cx);
737 }
738 }
739 }
741 }
742 let _ = window;
743 }
744
745 fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
746 window.focus(&self.focus);
747 self.goal_x = None;
748 let x = f32::from(ev.position.x) - f32::from(self.text_bounds.origin.x);
749 let y = f32::from(ev.position.y) - f32::from(self.text_bounds.origin.y);
750 let (line, col) = self.hit(x, y);
751
752 if !self.read_only && ev.click_count == 1 {
754 if let Some(row) = self.layout.get(line) {
755 if let RowKind::Task { .. } = row.plan.kind {
756 let in_slot = x < row.inset && x >= 0.0;
757 let in_first_row = y >= row.y && y < row.y + row.pad_top + row.line_h;
758 if in_slot && in_first_row && self.toggle_task(line, cx) {
759 return;
760 }
761 }
762 }
763 }
764 if ev.modifiers.platform || self.read_only {
766 if let (Some(row), Some(text)) = (self.layout.get(line), self.model.line(line)) {
767 if let Some(target) = row.plan.link_at(byte_for_col(text, col)) {
768 cx.emit(MarkdownEditorEvent::LinkClick(target.to_string()));
769 return;
770 }
771 }
772 }
773 match ev.click_count {
774 2 => {
775 self.model.move_to(line, col, false);
776 self.model.select_word();
777 }
778 n if n > 2 => {
779 self.model.move_to(line, col, false);
780 self.model.select_line();
781 }
782 _ => self.model.move_to(line, col, ev.modifiers.shift),
783 }
784 cx.notify();
785 }
786
787 fn on_drag_move(
788 &mut self,
789 ev: &DragMoveEvent<MarkdownDrag>,
790 _window: &mut Window,
791 cx: &mut Context<Self>,
792 ) {
793 if ev.drag(cx).0 != cx.entity_id() {
794 return;
795 }
796 let x = f32::from(ev.event.position.x) - f32::from(self.text_bounds.origin.x);
797 let y = f32::from(ev.event.position.y) - f32::from(self.text_bounds.origin.y);
798 let (line, col) = self.hit(x, y);
799 self.model.move_to(line, col, true);
800 self.scroll_to_cursor = true;
801 cx.notify();
802 }
803
804 fn hit(&self, x: f32, y: f32) -> (usize, usize) {
807 if self.layout.is_empty() {
808 return (0, 0);
809 }
810 let mut line = self.layout.len() - 1;
811 for (i, row) in self.layout.iter().enumerate() {
812 if y < row.y + row.height {
813 line = i;
814 break;
815 }
816 }
817 line = line.min(self.model.line_count().saturating_sub(1));
818 let row = &self.layout[line];
819 let Some(text) = self.model.line(line) else {
820 return (line, 0);
821 };
822 let text_h = (row.visual_rows() as f32 * row.line_h).max(row.line_h);
823 let local = point(
824 px((x - row.inset).max(0.0)),
825 px((y - row.y - row.pad_top).clamp(0.0, text_h - 1.0)),
826 );
827 let vis = match &row.text {
828 Some(shaped) => shaped
829 .closest_index_for_position(local, px(row.line_h))
830 .unwrap_or_else(|near| near),
831 None => 0,
832 };
833 let src = src_for_vis(&row.plan.segs, vis);
834 (line, col_for_byte(text, src))
835 }
836
837 fn move_visual(&mut self, down: bool, extend: bool, cx: &mut Context<Self>) {
840 let cursor = self.model.cursor();
841 if self.layout.len() != self.model.line_count() {
842 if down {
844 self.model.move_down(extend);
845 } else {
846 self.model.move_up(extend);
847 }
848 self.after_move(cx);
849 return;
850 }
851 let row = &self.layout[cursor.line];
852 let text = self.model.line(cursor.line).unwrap_or("");
853 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
854 let (x, vrow) = row.caret(vis);
855 let goal = self.goal_x.unwrap_or(row.inset + x);
856 self.goal_x = Some(goal);
857
858 let target = if down {
859 if vrow + 1 < row.visual_rows() {
860 Some((cursor.line, vrow + 1))
861 } else if cursor.line + 1 < self.layout.len() {
862 Some((cursor.line + 1, 0))
863 } else {
864 None
865 }
866 } else if vrow > 0 {
867 Some((cursor.line, vrow - 1))
868 } else if cursor.line > 0 {
869 Some((
870 cursor.line - 1,
871 self.layout[cursor.line - 1].visual_rows() - 1,
872 ))
873 } else {
874 None
875 };
876 let Some((tline, tvrow)) = target else {
877 if down {
878 self.model.doc_end(extend);
879 } else {
880 self.model.doc_start(extend);
881 }
882 self.after_move(cx);
883 return;
884 };
885 let trow = &self.layout[tline];
886 let local = point(
887 px((goal - trow.inset).max(0.0)),
888 px((tvrow as f32 + 0.5) * trow.line_h),
889 );
890 let vis = match &trow.text {
891 Some(shaped) => shaped
892 .closest_index_for_position(local, px(trow.line_h))
893 .unwrap_or_else(|near| near),
894 None => 0,
895 };
896 let src = src_for_vis(&trow.plan.segs, vis);
897 let col = col_for_byte(self.model.line(tline).unwrap_or(""), src);
898 self.model.move_to(tline, col, extend);
899 self.after_move(cx);
900 }
901
902 fn after_edit(&mut self, cx: &mut Context<Self>) {
903 cx.emit(MarkdownEditorEvent::Change(self.model.text()));
904 self.scroll_to_cursor = true;
905 cx.notify();
906 cx.stop_propagation();
907 }
908
909 fn after_move(&mut self, cx: &mut Context<Self>) {
910 self.scroll_to_cursor = true;
911 cx.notify();
912 cx.stop_propagation();
913 }
914
915 fn ensure_cursor_visible(&mut self) {
918 let cursor = self.model.cursor();
919 let Some(row) = self.layout.get(cursor.line) else {
920 return;
921 };
922 let view_h = f32::from(self.scroll.bounds().size.height);
923 if view_h <= 0.0 {
924 return;
925 }
926 let text = self.model.line(cursor.line).unwrap_or("");
927 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
928 let (_, vrow) = row.caret(vis);
929 let top = row.y + row.pad_top + vrow as f32 * row.line_h;
930 let bottom = top + row.line_h + 2.0 * PAD_Y;
931 let offset = self.scroll.offset();
932 let y = scroll_adjust(f32::from(offset.y), view_h, top, bottom);
933 if y != f32::from(offset.y) {
934 self.scroll.set_offset(point(offset.x, px(y)));
935 }
936 }
937}
938
939impl Render for MarkdownEditor {
940 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
941 let focused = self.focus.is_focused(window);
942 let base = self.font_size;
943
944 let t = theme(cx);
945 let style = self.style;
946 let is_dark = t.scheme.is_dark();
947 let frame_border = if focused {
948 t.primary().hsla()
949 } else {
950 t.border().hsla()
951 };
952 let bg = style.bg.unwrap_or_else(|| t.surface().hsla());
953 let text_color = style.text.unwrap_or_else(|| t.text().hsla());
954 let dimmed = t.dimmed().hsla();
955 let marker_color = t.dimmed().alpha(0.75);
956 let accent = style.accent.unwrap_or_else(|| t.primary().hsla());
957 let caret_color = style.caret.unwrap_or(accent);
958 let selection_bg = style.selection.unwrap_or_else(|| t.primary().alpha(0.25));
959 let code_bg = style
960 .code_bg
961 .unwrap_or_else(|| t.surface_hover().alpha(if is_dark { 0.45 } else { 0.6 }));
962 let highlight_bg = t
963 .color(crate::theme::ColorName::Yellow, if is_dark { 7 } else { 2 })
964 .alpha(if is_dark { 0.45 } else { 0.7 });
965 let placeholder_color = style.placeholder.unwrap_or_else(|| t.dimmed().hsla());
966 let rule_color = t.border().hsla();
967 let quote_bar = t.primary().alpha(0.55);
968 let radius = t.radius(t.default_radius);
969 let token_colors: [Hsla; 8] = TokenKind::ALL.map(|kind| token_color(kind, t));
970
971 let prose = window.text_style().font();
972 let mono = Font {
973 family: MONO_FAMILY.into(),
974 ..prose.clone()
975 };
976 let cell_w = {
977 let ts = window.text_system();
978 let font_id = ts.resolve_font(&prose);
979 ts.ch_advance(font_id, px(base))
980 .map(f32::from)
981 .unwrap_or(base * 0.55)
982 };
983 self.cell_w = cell_w;
984 let base_line_h = (base * 1.6).round();
985
986 let cursor = self.model.cursor();
987 let selection = self.model.selection();
988 let reveal_range = match selection {
989 _ if !focused || self.read_only => None,
990 Some((s, e)) => Some((s.line, e.line)),
991 None => Some((cursor.line, cursor.line)),
992 };
993 let show_caret = focused && !self.read_only;
994 let show_placeholder = self.model.is_empty() && !focused && !self.placeholder.is_empty();
995
996 let wrap_total = self.wrap_w.max(120.0);
998 let mut rows: Vec<Row> = Vec::with_capacity(self.model.line_count());
999 let mut doc_state = DocState::default();
1000 let mut hl_state = LineState::default();
1001 let mut y = 0.0;
1002 for (i, line) in self.model.lines().iter().enumerate() {
1003 let lang = doc_state.fence_lang().map(str::to_string);
1004 let block = classify(line, &mut doc_state);
1005 if matches!(block, Block::Fence { open: true, .. }) {
1006 hl_state = LineState::default();
1007 }
1008 let reveal = reveal_range.is_some_and(|(s, e)| i >= s && i <= e);
1009 let plan = plan(line, &block, lang.as_deref(), reveal);
1010
1011 let m = metrics(&plan.kind);
1012 let (scale, lh, pt, pb) = (m.scale, m.line_height, m.pad_top, m.pad_bottom);
1013 let size = (base * scale).round();
1014 let line_h = (size * lh).round();
1015 let pad_top = (base * pt).round();
1016 let pad_bottom = (base * pb).round();
1017
1018 let inset = match &plan.kind {
1019 RowKind::Bullet { cols } | RowKind::Task { cols, .. } => {
1020 *cols as f32 * cell_w + (base * 1.6).round()
1021 }
1022 RowKind::Ordered { cols, number } => {
1023 let shaped = window.text_system().shape_line(
1024 SharedString::from(format!("{number}.")),
1025 px(size),
1026 &[TextRun {
1027 len: format!("{number}.").len(),
1028 font: prose.clone(),
1029 color: Hsla::default(),
1030 background_color: None,
1031 underline: None,
1032 strikethrough: None,
1033 }],
1034 None,
1035 );
1036 *cols as f32 * cell_w + f32::from(shaped.width) + (base * 0.55).round()
1037 }
1038 RowKind::Quote { depth } => *depth as f32 * (base * 1.1).round(),
1039 RowKind::Code { .. } | RowKind::Fence { .. } => (base * 0.75).round(),
1040 _ => 0.0,
1041 };
1042 let right_pad = match &plan.kind {
1043 RowKind::Code { .. } | RowKind::Fence { .. } => inset,
1044 _ => 0.0,
1045 };
1046 let wrap = (wrap_total - inset - right_pad).max(60.0);
1047
1048 let runs = if let RowKind::Code { lang } = &plan.kind {
1049 let language = fence_language(lang.as_deref());
1050 let tokens = language.line(&plan.visible, &mut hl_state);
1051 cover(plan.visible.len(), &tokens)
1052 .into_iter()
1053 .map(|(len, kind)| TextRun {
1054 len,
1055 font: mono.clone(),
1056 color: kind.map_or(text_color, |k| token_colors[k.index()]),
1057 background_color: None,
1058 underline: None,
1059 strikethrough: None,
1060 })
1061 .collect::<Vec<_>>()
1062 } else {
1063 let heading = matches!(plan.kind, RowKind::Heading(_));
1064 plan
1065 .runs
1066 .iter()
1067 .map(|run| {
1068 let s = run.style;
1069 let font = Font {
1070 family: if s.code {
1071 MONO_FAMILY.into()
1072 } else {
1073 prose.family.clone()
1074 },
1075 weight: if heading || s.bold {
1076 FontWeight::BOLD
1077 } else {
1078 prose.weight
1079 },
1080 style: if s.italic {
1081 FontStyle::Italic
1082 } else {
1083 prose.style
1084 },
1085 ..prose.clone()
1086 };
1087 let color = if run.marker {
1088 marker_color
1089 } else if run.dim {
1090 dimmed
1091 } else if s.link {
1092 accent
1093 } else {
1094 text_color
1095 };
1096 TextRun {
1097 len: run.len,
1098 font,
1099 color,
1100 background_color: if s.highlight {
1101 Some(highlight_bg)
1102 } else if s.code {
1103 Some(code_bg)
1104 } else {
1105 None
1106 },
1107 underline: (s.link && !run.marker).then(|| UnderlineStyle {
1108 thickness: px(1.0),
1109 color: Some(accent),
1110 wavy: false,
1111 }),
1112 strikethrough: s.strike.then(|| StrikethroughStyle {
1113 thickness: px(1.0),
1114 color: Some(dimmed),
1115 }),
1116 }
1117 })
1118 .collect::<Vec<_>>()
1119 };
1120
1121 let shaped = window
1122 .text_system()
1123 .shape_text(
1124 SharedString::from(plan.visible.clone()),
1125 px(size),
1126 &runs,
1127 Some(px(wrap)),
1128 None,
1129 )
1130 .ok()
1131 .and_then(|mut lines| {
1132 if lines.is_empty() {
1133 None
1134 } else {
1135 Some(std::rc::Rc::new(lines.swap_remove(0)))
1136 }
1137 });
1138 let visual_rows = shaped
1139 .as_ref()
1140 .map_or(1, |s| s.wrap_boundaries().len() + 1)
1141 .max(1);
1142 let height = pad_top + visual_rows as f32 * line_h + pad_bottom;
1143 rows.push(Row {
1144 plan,
1145 text: shaped,
1146 line_h,
1147 pad_top,
1148 inset,
1149 height,
1150 y,
1151 });
1152 y += height;
1153 }
1154 self.layout = rows;
1155 if self.scroll_to_cursor {
1156 self.scroll_to_cursor = false;
1157 self.ensure_cursor_visible();
1158 }
1159
1160 let mut row_divs: Vec<Div> = Vec::with_capacity(self.layout.len());
1162 for (i, row) in self.layout.iter().enumerate() {
1163 let size_of_row = row
1164 .text
1165 .as_ref()
1166 .map(|t| f32::from(t.font_size()))
1167 .unwrap_or(base);
1168 let mut el = div().relative().w_full().h(px(row.height));
1169
1170 match &row.plan.kind {
1171 RowKind::Code { .. } => el = el.bg(code_bg),
1172 RowKind::Fence { open } => {
1173 el = el.bg(code_bg);
1174 el = if *open {
1175 el.rounded_t(px(radius))
1176 } else {
1177 el.rounded_b(px(radius))
1178 };
1179 }
1180 RowKind::Rule if !row.plan.revealed => {
1181 el = el.child(
1182 div()
1183 .absolute()
1184 .left_0()
1185 .right_0()
1186 .top(px((row.height / 2.0 - 1.0).max(0.0)))
1187 .h(px(2.0))
1188 .rounded(px(1.0))
1189 .bg(rule_color),
1190 );
1191 }
1192 RowKind::Quote { depth } => {
1193 let step = (base * 1.1).round();
1194 for k in 0..*depth {
1195 el = el.child(
1196 div()
1197 .absolute()
1198 .left(px(k as f32 * step + 1.0))
1199 .top_0()
1200 .bottom_0()
1201 .w(px(3.0))
1202 .rounded(px(1.5))
1203 .bg(quote_bar),
1204 );
1205 }
1206 }
1207 RowKind::Bullet { .. } => {
1208 let dot = (base * 0.36).round().max(4.0);
1209 el = el.child(
1210 div()
1211 .absolute()
1212 .left(px(row.inset - dot - (base * 0.6).round()))
1213 .top(px(row.pad_top + (row.line_h - dot) / 2.0))
1214 .size(px(dot))
1215 .rounded_full()
1216 .bg(marker_color),
1217 );
1218 }
1219 RowKind::Ordered { number, .. } => {
1220 el = el.child(
1221 div()
1222 .absolute()
1223 .left_0()
1224 .top(px(row.pad_top))
1225 .w(px((row.inset - (base * 0.35)).max(0.0)))
1226 .h(px(row.line_h))
1227 .flex()
1228 .items_center()
1229 .justify_end()
1230 .text_size(px(size_of_row))
1231 .text_color(marker_color)
1232 .child(SharedString::from(format!("{number}."))),
1233 );
1234 }
1235 RowKind::Task { checked, .. } => {
1236 let box_s = (size_of_row * 1.05).round();
1237 let boxed = div()
1238 .absolute()
1239 .left(px(row.inset - box_s - (base * 0.45).round()))
1240 .top(px(row.pad_top + (row.line_h - box_s) / 2.0))
1241 .size(px(box_s))
1242 .rounded(px(4.0))
1243 .flex()
1244 .items_center()
1245 .justify_center();
1246 el = el.child(if *checked {
1247 boxed
1248 .bg(accent)
1249 .text_size(px(box_s * 0.8))
1250 .text_color(gpui::white())
1251 .child(Glyph::Lucide(IconName::Check))
1252 } else {
1253 boxed.border_1().border_color(dimmed)
1254 });
1255 }
1256 _ => {}
1257 }
1258
1259 if let Some((start, end)) = selection {
1261 if let Some(text) = self.model.line(i) {
1262 if let Some((s_col, e_col, newline)) = line_selection(start, end, i, text.chars().count())
1263 {
1264 let vs = vis_for_src(&row.plan.segs, byte_for_col(text, s_col));
1265 let ve = vis_for_src(&row.plan.segs, byte_for_col(text, e_col));
1266 for (x, vrow, w) in row.sel_rects(vs, ve, newline, cell_w) {
1267 el = el.child(
1268 div()
1269 .absolute()
1270 .left(px(row.inset + x))
1271 .top(px(row.pad_top + vrow as f32 * row.line_h))
1272 .w(px(w.max(2.0)))
1273 .h(px(row.line_h))
1274 .bg(selection_bg),
1275 );
1276 }
1277 }
1278 }
1279 }
1280
1281 if let Some(shaped) = row.text.clone() {
1284 if !row.plan.visible.is_empty() {
1285 let line_h = px(row.line_h);
1286 let text_h = row.visual_rows() as f32 * row.line_h;
1287 el = el.child(
1288 div()
1289 .absolute()
1290 .left(px(row.inset))
1291 .top(px(row.pad_top))
1292 .w(px((wrap_total - row.inset).max(60.0)))
1293 .h(px(text_h))
1294 .child(canvas(
1295 |_, _, _| (),
1296 move |bounds, _, window, cx| {
1297 let origin = bounds.origin;
1298 shaped
1299 .paint_background(origin, line_h, TextAlign::Left, None, window, cx)
1300 .ok();
1301 shaped
1302 .paint(origin, line_h, TextAlign::Left, None, window, cx)
1303 .ok();
1304 },
1305 )),
1306 );
1307 }
1308 }
1309
1310 if show_caret && i == cursor.line {
1312 if let Some(text) = self.model.line(i) {
1313 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
1314 let (x, vrow) = row.caret(vis);
1315 el = el.child(
1316 div()
1317 .absolute()
1318 .left(px((row.inset + x - 1.0).max(0.0)))
1319 .top(px(row.pad_top + vrow as f32 * row.line_h))
1320 .w(px(2.0))
1321 .h(px(row.line_h))
1322 .bg(caret_color),
1323 );
1324 }
1325 }
1326
1327 row_divs.push(el);
1328 }
1329
1330 let entity = cx.entity();
1333 let probe = canvas(
1334 move |bounds, _window, cx| {
1335 entity.update(cx, |this, cx| {
1336 this.text_bounds = bounds;
1337 let w = f32::from(bounds.size.width);
1338 if (w - this.wrap_w).abs() > 0.5 {
1339 this.wrap_w = w;
1340 cx.notify();
1341 }
1342 });
1343 },
1344 |_, _, _, _| {},
1345 )
1346 .absolute()
1347 .size_full();
1348
1349 let mut lines_col = div()
1350 .relative()
1351 .flex()
1352 .flex_col()
1353 .w_full()
1354 .child(probe)
1355 .children(row_divs);
1356 if show_placeholder {
1357 lines_col = lines_col.child(
1358 div()
1359 .absolute()
1360 .top_0()
1361 .left_0()
1362 .text_color(placeholder_color)
1363 .child(self.placeholder.clone()),
1364 );
1365 }
1366
1367 let content = div().w_full().py(px(PAD_Y)).px(px(PAD_X)).child(lines_col);
1368
1369 let mut body = div()
1370 .id("guise-markdown-body")
1371 .track_focus(&self.focus)
1372 .on_key_down(cx.listener(Self::on_key))
1373 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1374 .on_drag(MarkdownDrag(cx.entity_id()), |_, _, _, cx| {
1375 cx.new(|_| Empty)
1376 })
1377 .on_drag_move(cx.listener(Self::on_drag_move))
1378 .overflow_y_scroll()
1379 .track_scroll(&self.scroll)
1380 .w_full()
1381 .max_h_full()
1382 .cursor_text()
1383 .child(content);
1384 if let Some(rows) = self.rows {
1385 body = body.min_h(px(rows as f32 * base_line_h + 2.0 * PAD_Y));
1386 }
1387
1388 let mut frame = div().flex().flex_col().w_full().h_full();
1389 if !style.bare {
1390 frame = frame
1391 .rounded(px(radius))
1392 .border_1()
1393 .border_color(frame_border);
1394 }
1395 frame
1396 .bg(bg)
1397 .overflow_hidden()
1398 .text_size(px(base))
1399 .line_height(px(base_line_h))
1400 .text_color(text_color)
1401 .child(body)
1402 .probe("MarkdownEditor")
1403 }
1404}
1405
1406fn fence_language(lang: Option<&str>) -> Language {
1410 match lang {
1411 Some("rust" | "rs") => Language::Rust,
1412 Some("sql") => Language::Sql,
1413 Some("json" | "jsonc") => Language::Json,
1414 _ => Language::None,
1415 }
1416}
1417
1418fn classify_alone(line: &str) -> Block {
1421 let mut state = DocState::default();
1422 classify("", &mut state);
1423 classify(line, &mut state)
1424}
1425
1426fn continuation(line: &str) -> Option<String> {
1430 match classify_alone(line) {
1431 Block::Task { indent, .. } => {
1432 let ch = line.as_bytes()[indent] as char;
1433 Some(format!("{ch} [ ] "))
1434 }
1435 Block::Bullet { indent, .. } => {
1436 let ch = line.as_bytes()[indent] as char;
1437 Some(format!("{ch} "))
1438 }
1439 Block::Ordered { indent, number, .. } => {
1440 let delim = line[indent..]
1441 .bytes()
1442 .find(|b| *b == b'.' || *b == b')')
1443 .unwrap_or(b'.') as char;
1444 Some(format!("{}{delim} ", number + 1))
1445 }
1446 Block::Quote { depth, .. } => Some("> ".repeat(depth as usize)),
1447 _ => None,
1448 }
1449}
1450
1451fn prefix_end(line: &str) -> usize {
1453 match classify_alone(line) {
1454 Block::Task { content, .. }
1455 | Block::Bullet { content, .. }
1456 | Block::Ordered { content, .. }
1457 | Block::Quote { content, .. } => content,
1458 _ => 0,
1459 }
1460}
1461
1462fn split_visual(bounds: &[usize], vs: usize, ve: usize) -> (usize, usize) {
1466 let sr = bounds.iter().filter(|&&b| b <= vs).count();
1467 let er = bounds.iter().filter(|&&b| b < ve).count();
1468 (sr, er.max(sr))
1469}
1470
1471fn cover(
1474 len: usize,
1475 tokens: &[(std::ops::Range<usize>, TokenKind)],
1476) -> Vec<(usize, Option<TokenKind>)> {
1477 let mut out = Vec::new();
1478 let mut at = 0;
1479 for (range, kind) in tokens {
1480 let start = range.start.max(at).min(len);
1481 let end = range.end.max(start).min(len);
1482 if start > at {
1483 out.push((start - at, None));
1484 }
1485 if end > start {
1486 out.push((end - start, Some(*kind)));
1487 }
1488 at = end.max(at);
1489 }
1490 if at < len {
1491 out.push((len - at, None));
1492 }
1493 out
1494}
1495
1496fn line_selection(
1500 start: Pos,
1501 end: Pos,
1502 line: usize,
1503 line_len: usize,
1504) -> Option<(usize, usize, bool)> {
1505 if line < start.line || line > end.line {
1506 return None;
1507 }
1508 let s = if line == start.line {
1509 start.col.min(line_len)
1510 } else {
1511 0
1512 };
1513 let e = if line == end.line {
1514 end.col.min(line_len)
1515 } else {
1516 line_len
1517 };
1518 let e = e.max(s);
1519 let newline = line < end.line;
1520 if e == s && !newline {
1521 return None;
1522 }
1523 Some((s, e, newline))
1524}
1525
1526fn scroll_adjust(offset: f32, view: f32, top: f32, bottom: f32) -> f32 {
1529 let mut adjusted = offset;
1530 if bottom + adjusted > view {
1531 adjusted = view - bottom;
1532 }
1533 if top + adjusted < 0.0 {
1534 adjusted = -top;
1535 }
1536 adjusted
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541 use super::*;
1542
1543 #[test]
1544 fn continuation_markers() {
1545 assert_eq!(continuation("- item"), Some("- ".into()));
1546 assert_eq!(continuation("* item"), Some("* ".into()));
1547 assert_eq!(continuation(" - item"), Some("- ".into()));
1548 assert_eq!(continuation("- [x] done"), Some("- [ ] ".into()));
1549 assert_eq!(continuation("3. third"), Some("4. ".into()));
1550 assert_eq!(continuation("3) third"), Some("4) ".into()));
1551 assert_eq!(continuation("> quoted"), Some("> ".into()));
1552 assert_eq!(continuation("> > deep"), Some("> > ".into()));
1553 assert_eq!(continuation("plain"), None);
1554 assert_eq!(continuation("# heading"), None);
1555 }
1556
1557 #[test]
1558 fn prefix_end_finds_content() {
1559 assert_eq!(prefix_end("- item"), 2);
1560 assert_eq!(prefix_end(" - [ ] x"), 8);
1561 assert_eq!(prefix_end("> q"), 2);
1562 assert_eq!(prefix_end("plain"), 0);
1563 }
1564
1565 #[test]
1566 fn split_visual_rows() {
1567 assert_eq!(split_visual(&[], 0, 10), (0, 0));
1569 assert_eq!(split_visual(&[10], 2, 8), (0, 0));
1571 assert_eq!(split_visual(&[10], 2, 15), (0, 1));
1572 assert_eq!(split_visual(&[10], 12, 15), (1, 1));
1573 assert_eq!(split_visual(&[10], 10, 15), (1, 1));
1575 assert_eq!(split_visual(&[10], 2, 10), (0, 0));
1576 assert_eq!(split_visual(&[10], 10, 10), (1, 1));
1578 }
1579
1580 #[test]
1581 fn cover_spans_exactly() {
1582 let tokens = vec![(2..5, TokenKind::Keyword)];
1583 let s = cover(10, &tokens);
1584 let total: usize = s.iter().map(|(len, _)| len).sum();
1585 assert_eq!(total, 10);
1586 assert_eq!(s[1], (3, Some(TokenKind::Keyword)));
1587 assert_eq!(cover(4, &[]), vec![(4, None)]);
1588 }
1589
1590 #[test]
1591 fn fence_language_mapping() {
1592 assert_eq!(fence_language(Some("rust")), Language::Rust);
1593 assert_eq!(fence_language(Some("rs")), Language::Rust);
1594 assert_eq!(fence_language(Some("json")), Language::Json);
1595 assert_eq!(fence_language(Some("python")), Language::None);
1596 assert_eq!(fence_language(None), Language::None);
1597 }
1598
1599 #[test]
1600 fn scroll_adjust_reveals_target() {
1601 assert_eq!(scroll_adjust(-10.0, 100.0, 20.0, 40.0), -10.0);
1602 assert_eq!(scroll_adjust(-50.0, 100.0, 20.0, 40.0), -20.0);
1603 assert_eq!(scroll_adjust(0.0, 100.0, 150.0, 170.0), -70.0);
1604 }
1605
1606 fn at(line: usize, col: usize) -> Pos {
1607 Pos::new(line, col)
1608 }
1609
1610 #[test]
1611 fn line_selection_matches_editor_semantics() {
1612 assert_eq!(
1613 line_selection(at(1, 2), at(1, 5), 1, 8),
1614 Some((2, 5, false))
1615 );
1616 assert_eq!(line_selection(at(0, 3), at(2, 2), 1, 4), Some((0, 4, true)));
1617 assert_eq!(line_selection(at(0, 3), at(2, 2), 3, 4), None);
1618 assert_eq!(line_selection(at(0, 0), at(2, 1), 1, 0), Some((0, 0, true)));
1619 }
1620}