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.text
97 .as_ref()
98 .map_or(1, |t| t.wrap_boundaries().len() + 1)
99 }
100
101 fn boundaries(&self) -> Vec<usize> {
103 let Some(text) = &self.text else {
104 return Vec::new();
105 };
106 text.wrap_boundaries()
107 .iter()
108 .map(|b| text.runs()[b.run_ix].glyphs[b.glyph_ix].index)
109 .collect()
110 }
111
112 fn pos_end(&self, vis: usize) -> (f32, f32) {
115 let Some(text) = &self.text else {
116 return (0.0, 0.0);
117 };
118 match text.position_for_index(vis.min(text.len()), px(self.line_h)) {
119 Some(p) => (f32::from(p.x), f32::from(p.y)),
120 None => (0.0, 0.0),
121 }
122 }
123
124 fn caret(&self, vis: usize) -> (f32, usize) {
127 let bounds = self.boundaries();
128 let row = bounds.iter().filter(|&&b| b <= vis).count();
129 if bounds.contains(&vis) {
130 return (0.0, row);
131 }
132 let (x, y) = self.pos_end(vis);
133 (
134 x,
135 if self.line_h > 0.0 {
136 (y / self.line_h).round() as usize
137 } else {
138 0
139 },
140 )
141 }
142
143 fn sel_rects(&self, vs: usize, ve: usize, newline: bool, cell: f32) -> Vec<(f32, usize, f32)> {
146 let bounds = self.boundaries();
147 let (sr, er) = split_visual(&bounds, vs, ve);
148 let (sx, _) = if bounds.contains(&vs) {
149 (0.0, 0.0)
150 } else {
151 self.pos_end(vs)
152 };
153 let (ex, _) = self.pos_end(ve);
154 let row_end = |r: usize| -> f32 {
155 match bounds.get(r) {
156 Some(&b) => self.pos_end(b).0,
157 None => self.pos_end(usize::MAX).0,
158 }
159 };
160 let mut rects = Vec::new();
161 if sr == er {
162 rects.push((sx, sr, (ex - sx).max(0.0)));
163 } else {
164 rects.push((sx, sr, (row_end(sr) - sx).max(0.0)));
165 for r in sr + 1..er {
166 rects.push((0.0, r, row_end(r).max(0.0)));
167 }
168 rects.push((0.0, er, ex.max(0.0)));
169 }
170 if newline {
171 if let Some(last) = rects.last_mut() {
172 last.2 += cell;
173 }
174 }
175 rects.retain(|r| r.2 > 0.0);
176 rects
177 }
178}
179
180pub struct MarkdownEditor {
189 model: EditorModel,
190 placeholder: SharedString,
191 read_only: bool,
192 font_size: f32,
193 rows: Option<usize>,
194 style: MarkdownStyle,
195 focus: FocusHandle,
196 scroll: ScrollHandle,
197 text_bounds: Bounds<Pixels>,
199 wrap_w: f32,
201 cell_w: f32,
203 layout: Vec<Row>,
205 scroll_to_cursor: bool,
207 goal_x: Option<f32>,
209}
210
211impl EventEmitter<MarkdownEditorEvent> for MarkdownEditor {}
212
213impl MarkdownEditor {
214 pub fn new(cx: &mut Context<Self>) -> Self {
215 MarkdownEditor {
216 model: EditorModel::new(""),
217 placeholder: SharedString::default(),
218 read_only: false,
219 font_size: 15.0,
220 rows: None,
221 style: MarkdownStyle::default(),
222 focus: cx.focus_handle(),
223 scroll: ScrollHandle::new(),
224 text_bounds: Bounds::default(),
225 wrap_w: DEFAULT_WRAP,
226 cell_w: 15.0 * 0.55,
227 layout: Vec::new(),
228 scroll_to_cursor: false,
229 goal_x: None,
230 }
231 }
232
233 pub fn value(mut self, text: &str) -> Self {
237 self.model.set_text(text);
238 self
239 }
240
241 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
243 self.placeholder = placeholder.into();
244 self
245 }
246
247 pub fn read_only(mut self, read_only: bool) -> Self {
250 self.read_only = read_only;
251 self
252 }
253
254 pub fn font_size(mut self, size: f32) -> Self {
256 self.font_size = size;
257 self
258 }
259
260 pub fn rows(mut self, rows: usize) -> Self {
262 self.rows = Some(rows);
263 self
264 }
265
266 pub fn tab_size(mut self, n: usize) -> Self {
268 self.model.set_tab_size(n);
269 self
270 }
271
272 pub fn style(mut self, style: MarkdownStyle) -> Self {
274 self.style = style;
275 self
276 }
277
278 pub fn set_style(&mut self, style: MarkdownStyle, cx: &mut Context<Self>) {
280 self.style = style;
281 cx.notify();
282 }
283
284 pub fn text(&self) -> String {
288 self.model.text()
289 }
290
291 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
293 self.model.set_text(value);
294 cx.notify();
295 }
296
297 pub fn focus_handle(&self) -> FocusHandle {
299 self.focus.clone()
300 }
301
302 pub fn model(&self) -> &EditorModel {
305 &self.model
306 }
307
308 pub fn edit<R>(&mut self, cx: &mut Context<Self>, f: impl FnOnce(&mut EditorModel) -> R) -> R {
312 let before = self.model.text();
313 let result = f(&mut self.model);
314 let after = self.model.text();
315 if after != before {
316 cx.emit(MarkdownEditorEvent::Change(after));
317 }
318 self.scroll_to_cursor = true;
319 cx.notify();
320 result
321 }
322
323 pub fn bind(entity: &Entity<MarkdownEditor>, signal: &Signal<String>, cx: &mut App) {
327 let initial = signal.get(cx);
328 entity.update(cx, |this, cx| {
329 if this.text() != initial {
330 this.set_text(&initial, cx);
331 }
332 });
333 let sink = signal.clone();
334 cx.subscribe(entity, move |_editor, event: &MarkdownEditorEvent, cx| {
335 if let MarkdownEditorEvent::Change(text) = event {
336 sink.set_if_changed(cx, text.clone());
337 }
338 })
339 .detach();
340 let editor = entity.downgrade();
343 cx.observe(signal.entity(), move |observed, cx| {
344 let value = observed.read(cx).clone();
345 editor
346 .update(cx, |this, cx| {
347 if this.text() != value {
348 this.set_text(&value, cx);
349 }
350 })
351 .ok();
352 })
353 .detach();
354 }
355
356 pub fn toggle_task(&mut self, line: usize, cx: &mut Context<Self>) -> bool {
361 let Some(text) = self.model.line(line) else {
362 return false;
363 };
364 let Block::Task { checked, state, .. } = classify_alone(text) else {
365 return false;
366 };
367 let cursor = self.model.cursor();
368 self.edit(cx, |m| {
369 m.move_to(line, state, false);
371 m.move_to(line, state + 1, true);
372 m.insert(if checked { " " } else { "x" });
373 m.move_to(cursor.line, cursor.col, false);
374 });
375 true
376 }
377
378 fn toggle_wrap(&mut self, marker: &str, cx: &mut Context<Self>) {
381 if self.read_only {
382 return;
383 }
384 let chars = marker.chars().count();
385 if self.model.selection().is_none() {
386 self.model.select_word();
387 }
388 let Some((start, end)) = self.model.selection() else {
389 self.edit(cx, |m| {
391 m.insert(&format!("{marker}{marker}"));
392 for _ in 0..chars {
393 m.move_left(false);
394 }
395 });
396 return;
397 };
398 if start.line != end.line {
399 return;
400 }
401 let line = self.model.line(start.line).unwrap_or("");
403 let (sb, eb) = (byte_for_col(line, start.col), byte_for_col(line, end.col));
404 if line[..sb].ends_with(marker) && line[eb..].starts_with(marker) {
405 self.model.move_to(start.line, start.col - chars, false);
406 self.model.move_to(end.line, end.col + chars, true);
407 }
408 let Some(sel) = self.model.selected_text() else {
409 return;
410 };
411 let unwrap =
412 sel.starts_with(marker) && sel.ends_with(marker) && sel.len() >= 2 * marker.len();
413 let replacement = if unwrap {
414 sel[marker.len()..sel.len() - marker.len()].to_string()
415 } else {
416 format!("{marker}{sel}{marker}")
417 };
418 self.edit(cx, |m| m.insert(&replacement));
419 }
420
421 fn insert_link(&mut self, cx: &mut Context<Self>) {
424 if self.read_only {
425 return;
426 }
427 let single_line = matches!(self.model.selection(), Some((s, e)) if s.line == e.line);
428 self.edit(cx, |m| {
429 if single_line {
430 let sel = m.selected_text().unwrap_or_default();
431 m.insert(&format!("[{sel}]()"));
432 m.move_left(false);
433 } else {
434 m.insert("[]()");
435 for _ in 0..3 {
436 m.move_left(false);
437 }
438 }
439 });
440 }
441
442 fn on_enter(&mut self, cx: &mut Context<Self>) {
444 let cursor = self.model.cursor();
445 let line = self.model.line(cursor.line).unwrap_or("").to_string();
446 let in_code = matches!(
447 self.layout.get(cursor.line).map(|r| &r.plan.kind),
448 Some(RowKind::Code { .. } | RowKind::Fence { .. } | RowKind::FrontMatter)
449 );
450 let marker = if in_code || self.model.selection().is_some() {
451 None
452 } else {
453 continuation(&line)
454 };
455 match marker {
456 Some(_) if line[prefix_end(&line)..].trim().is_empty() => {
457 let cols = line.chars().count();
460 self.edit(cx, |m| {
461 m.move_to(cursor.line, 0, false);
462 m.move_to(cursor.line, cols, true);
463 m.delete_selection();
464 });
465 }
466 Some(marker) => self.edit(cx, |m| {
467 m.newline();
468 m.insert(&marker);
469 }),
470 None => self.edit(cx, |m| m.newline()),
471 }
472 }
473
474 fn on_tab(&mut self, outdent: bool, cx: &mut Context<Self>) {
476 let cursor = self.model.cursor();
477 let line = self.model.line(cursor.line).unwrap_or("").to_string();
478 let is_item = matches!(
479 classify_alone(&line),
480 Block::Bullet { .. } | Block::Ordered { .. } | Block::Task { .. }
481 );
482 if !is_item {
483 if !outdent {
484 self.edit(cx, |m| m.tab());
485 }
486 return;
487 }
488 let n = self.model.tab_size();
489 if outdent {
490 let lead = line.chars().take_while(|&c| c == ' ').count().min(n);
491 if lead == 0 {
492 return;
493 }
494 self.edit(cx, |m| {
495 m.move_to(cursor.line, 0, false);
496 m.move_to(cursor.line, lead, true);
497 m.delete_selection();
498 m.move_to(cursor.line, cursor.col.saturating_sub(lead), false);
499 });
500 } else {
501 self.edit(cx, |m| {
502 m.move_to(cursor.line, 0, false);
503 m.insert(&" ".repeat(n));
504 m.move_to(cursor.line, cursor.col + n, false);
505 });
506 }
507 }
508
509 fn backspace_marker(&mut self, cx: &mut Context<Self>) -> bool {
512 let cursor = self.model.cursor();
513 let line = self.model.line(cursor.line).unwrap_or("").to_string();
514 let content = match classify_alone(&line) {
515 Block::Bullet { content, .. }
516 | Block::Ordered { content, .. }
517 | Block::Task { content, .. }
518 | Block::Quote { content, .. } => content,
519 _ => return false,
520 };
521 if content == 0 || cursor.col != content {
523 return false;
524 }
525 self.edit(cx, |m| {
526 m.move_to(cursor.line, 0, false);
527 m.move_to(cursor.line, content, true);
528 m.delete_selection();
529 });
530 true
531 }
532
533 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
536 let ks = &event.keystroke;
537 let m = ks.modifiers;
538 let shift = m.shift;
539 if !matches!(ks.key.as_str(), "up" | "down") {
540 self.goal_x = None;
541 }
542 match ks.key.as_str() {
543 "left" => {
544 if m.platform {
545 self.model.home(shift);
546 } else if m.alt {
547 self.model.word_left(shift);
548 } else {
549 self.model.move_left(shift);
550 }
551 self.after_move(cx);
552 }
553 "right" => {
554 if m.platform {
555 self.model.end(shift);
556 } else if m.alt {
557 self.model.word_right(shift);
558 } else {
559 self.model.move_right(shift);
560 }
561 self.after_move(cx);
562 }
563 "up" => {
564 if m.platform {
565 self.model.doc_start(shift);
566 self.after_move(cx);
567 } else {
568 self.move_visual(false, shift, cx);
569 }
570 }
571 "down" => {
572 if m.platform {
573 self.model.doc_end(shift);
574 self.after_move(cx);
575 } else {
576 self.move_visual(true, shift, cx);
577 }
578 }
579 "home" => {
580 if m.platform {
581 self.model.doc_start(shift);
582 } else {
583 self.model.home(shift);
584 }
585 self.after_move(cx);
586 }
587 "end" => {
588 if m.platform {
589 self.model.doc_end(shift);
590 } else {
591 self.model.end(shift);
592 }
593 self.after_move(cx);
594 }
595 "backspace" => {
596 if self.read_only {
597 return;
598 }
599 if self.model.selection().is_none() && !m.platform && !m.alt {
600 if self.backspace_marker(cx) {
601 cx.stop_propagation();
602 return;
603 }
604 }
605 let changed = if self.model.selection().is_some() {
606 self.model.delete_selection()
607 } else if m.platform {
608 self.model.home(true);
609 self.model.delete_selection()
610 } else if m.alt {
611 self.model.word_left(true);
612 self.model.delete_selection()
613 } else {
614 self.model.backspace()
615 };
616 if changed {
617 self.after_edit(cx);
618 } else {
619 cx.stop_propagation();
620 }
621 }
622 "delete" => {
623 if self.read_only {
624 return;
625 }
626 let changed = if self.model.selection().is_some() {
627 self.model.delete_selection()
628 } else if m.platform {
629 self.model.end(true);
630 self.model.delete_selection()
631 } else if m.alt {
632 self.model.word_right(true);
633 self.model.delete_selection()
634 } else {
635 self.model.delete()
636 };
637 if changed {
638 self.after_edit(cx);
639 } else {
640 cx.stop_propagation();
641 }
642 }
643 "enter" if m.platform => {
644 if !self.read_only && self.toggle_task(self.model.cursor().line, cx) {
645 cx.stop_propagation();
646 }
647 }
648 "enter" => {
649 if self.read_only {
650 return;
651 }
652 self.on_enter(cx);
653 cx.stop_propagation();
654 }
655 "tab" => {
656 if m.platform || self.read_only {
657 return;
658 }
659 self.on_tab(shift, cx);
660 cx.stop_propagation();
661 }
662 "escape" => {
664 if self.model.selection().is_some() {
665 self.model.clear_selection();
666 cx.notify();
667 }
668 }
669 "a" if m.platform => {
670 self.model.select_all();
671 cx.notify();
672 cx.stop_propagation();
673 }
674 "b" if m.platform => {
675 self.toggle_wrap("**", cx);
676 cx.stop_propagation();
677 }
678 "i" if m.platform => {
679 self.toggle_wrap("*", cx);
680 cx.stop_propagation();
681 }
682 "k" if m.platform => {
683 self.insert_link(cx);
684 cx.stop_propagation();
685 }
686 "c" if m.platform => {
687 if let Some(text) = self.model.copy() {
688 cx.write_to_clipboard(ClipboardItem::new_string(text));
689 }
690 cx.stop_propagation();
691 }
692 "x" if m.platform => {
693 if self.read_only {
694 if let Some(text) = self.model.copy() {
696 cx.write_to_clipboard(ClipboardItem::new_string(text));
697 }
698 } else if let Some(text) = self.model.cut() {
699 cx.write_to_clipboard(ClipboardItem::new_string(text));
700 self.after_edit(cx);
701 return;
702 }
703 cx.stop_propagation();
704 }
705 "v" if m.platform => {
706 if !self.read_only {
707 if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
708 if !text.is_empty() {
709 self.model.insert(&text);
710 self.after_edit(cx);
711 return;
712 }
713 }
714 }
715 cx.stop_propagation();
716 }
717 "z" if m.platform => {
718 if !self.read_only {
719 let changed = if m.shift {
720 self.model.redo()
721 } else {
722 self.model.undo()
723 };
724 if changed {
725 self.after_edit(cx);
726 return;
727 }
728 }
729 cx.stop_propagation();
730 }
731 _ => {
732 if !self.read_only && !m.platform && !m.control {
735 if let Some(text) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
736 self.model.insert(text);
737 self.after_edit(cx);
738 }
739 }
740 }
742 }
743 let _ = window;
744 }
745
746 fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
747 window.focus(&self.focus);
748 self.goal_x = None;
749 let x = f32::from(ev.position.x) - f32::from(self.text_bounds.origin.x);
750 let y = f32::from(ev.position.y) - f32::from(self.text_bounds.origin.y);
751 let (line, col) = self.hit(x, y);
752
753 if !self.read_only && ev.click_count == 1 {
755 if let Some(row) = self.layout.get(line) {
756 if let RowKind::Task { .. } = row.plan.kind {
757 let in_slot = x < row.inset && x >= 0.0;
758 let in_first_row = y >= row.y && y < row.y + row.pad_top + row.line_h;
759 if in_slot && in_first_row && self.toggle_task(line, cx) {
760 return;
761 }
762 }
763 }
764 }
765 if ev.modifiers.platform || self.read_only {
767 if let (Some(row), Some(text)) = (self.layout.get(line), self.model.line(line)) {
768 if let Some(target) = row.plan.link_at(byte_for_col(text, col)) {
769 cx.emit(MarkdownEditorEvent::LinkClick(target.to_string()));
770 return;
771 }
772 }
773 }
774 match ev.click_count {
775 2 => {
776 self.model.move_to(line, col, false);
777 self.model.select_word();
778 }
779 n if n > 2 => {
780 self.model.move_to(line, col, false);
781 self.model.select_line();
782 }
783 _ => self.model.move_to(line, col, ev.modifiers.shift),
784 }
785 cx.notify();
786 }
787
788 fn on_drag_move(
789 &mut self,
790 ev: &DragMoveEvent<MarkdownDrag>,
791 _window: &mut Window,
792 cx: &mut Context<Self>,
793 ) {
794 if ev.drag(cx).0 != cx.entity_id() {
795 return;
796 }
797 let x = f32::from(ev.event.position.x) - f32::from(self.text_bounds.origin.x);
798 let y = f32::from(ev.event.position.y) - f32::from(self.text_bounds.origin.y);
799 let (line, col) = self.hit(x, y);
800 self.model.move_to(line, col, true);
801 self.scroll_to_cursor = true;
802 cx.notify();
803 }
804
805 fn hit(&self, x: f32, y: f32) -> (usize, usize) {
808 if self.layout.is_empty() {
809 return (0, 0);
810 }
811 let mut line = self.layout.len() - 1;
812 for (i, row) in self.layout.iter().enumerate() {
813 if y < row.y + row.height {
814 line = i;
815 break;
816 }
817 }
818 line = line.min(self.model.line_count().saturating_sub(1));
819 let row = &self.layout[line];
820 let Some(text) = self.model.line(line) else {
821 return (line, 0);
822 };
823 let text_h = (row.visual_rows() as f32 * row.line_h).max(row.line_h);
824 let local = point(
825 px((x - row.inset).max(0.0)),
826 px((y - row.y - row.pad_top).clamp(0.0, text_h - 1.0)),
827 );
828 let vis = match &row.text {
829 Some(shaped) => shaped
830 .closest_index_for_position(local, px(row.line_h))
831 .unwrap_or_else(|near| near),
832 None => 0,
833 };
834 let src = src_for_vis(&row.plan.segs, vis);
835 (line, col_for_byte(text, src))
836 }
837
838 fn move_visual(&mut self, down: bool, extend: bool, cx: &mut Context<Self>) {
841 let cursor = self.model.cursor();
842 if self.layout.len() != self.model.line_count() {
843 if down {
845 self.model.move_down(extend);
846 } else {
847 self.model.move_up(extend);
848 }
849 self.after_move(cx);
850 return;
851 }
852 let row = &self.layout[cursor.line];
853 let text = self.model.line(cursor.line).unwrap_or("");
854 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
855 let (x, vrow) = row.caret(vis);
856 let goal = self.goal_x.unwrap_or(row.inset + x);
857 self.goal_x = Some(goal);
858
859 let target = if down {
860 if vrow + 1 < row.visual_rows() {
861 Some((cursor.line, vrow + 1))
862 } else if cursor.line + 1 < self.layout.len() {
863 Some((cursor.line + 1, 0))
864 } else {
865 None
866 }
867 } else if vrow > 0 {
868 Some((cursor.line, vrow - 1))
869 } else if cursor.line > 0 {
870 Some((
871 cursor.line - 1,
872 self.layout[cursor.line - 1].visual_rows() - 1,
873 ))
874 } else {
875 None
876 };
877 let Some((tline, tvrow)) = target else {
878 if down {
879 self.model.doc_end(extend);
880 } else {
881 self.model.doc_start(extend);
882 }
883 self.after_move(cx);
884 return;
885 };
886 let trow = &self.layout[tline];
887 let local = point(
888 px((goal - trow.inset).max(0.0)),
889 px((tvrow as f32 + 0.5) * trow.line_h),
890 );
891 let vis = match &trow.text {
892 Some(shaped) => shaped
893 .closest_index_for_position(local, px(trow.line_h))
894 .unwrap_or_else(|near| near),
895 None => 0,
896 };
897 let src = src_for_vis(&trow.plan.segs, vis);
898 let col = col_for_byte(self.model.line(tline).unwrap_or(""), src);
899 self.model.move_to(tline, col, extend);
900 self.after_move(cx);
901 }
902
903 fn after_edit(&mut self, cx: &mut Context<Self>) {
904 cx.emit(MarkdownEditorEvent::Change(self.model.text()));
905 self.scroll_to_cursor = true;
906 cx.notify();
907 cx.stop_propagation();
908 }
909
910 fn after_move(&mut self, cx: &mut Context<Self>) {
911 self.scroll_to_cursor = true;
912 cx.notify();
913 cx.stop_propagation();
914 }
915
916 fn ensure_cursor_visible(&mut self) {
919 let cursor = self.model.cursor();
920 let Some(row) = self.layout.get(cursor.line) else {
921 return;
922 };
923 let view_h = f32::from(self.scroll.bounds().size.height);
924 if view_h <= 0.0 {
925 return;
926 }
927 let text = self.model.line(cursor.line).unwrap_or("");
928 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
929 let (_, vrow) = row.caret(vis);
930 let top = row.y + row.pad_top + vrow as f32 * row.line_h;
931 let bottom = top + row.line_h + 2.0 * PAD_Y;
932 let offset = self.scroll.offset();
933 let y = scroll_adjust(f32::from(offset.y), view_h, top, bottom);
934 if y != f32::from(offset.y) {
935 self.scroll.set_offset(point(offset.x, px(y)));
936 }
937 }
938}
939
940impl Render for MarkdownEditor {
941 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
942 let focused = self.focus.is_focused(window);
943 let base = self.font_size;
944
945 let t = theme(cx);
946 let style = self.style;
947 let is_dark = t.scheme.is_dark();
948 let frame_border = if focused {
949 t.primary().hsla()
950 } else {
951 t.border().hsla()
952 };
953 let bg = style.bg.unwrap_or_else(|| t.surface().hsla());
954 let text_color = style.text.unwrap_or_else(|| t.text().hsla());
955 let dimmed = t.dimmed().hsla();
956 let marker_color = t.dimmed().alpha(0.75);
957 let accent = style.accent.unwrap_or_else(|| t.primary().hsla());
958 let caret_color = style.caret.unwrap_or(accent);
959 let selection_bg = style.selection.unwrap_or_else(|| t.primary().alpha(0.25));
960 let code_bg = style
961 .code_bg
962 .unwrap_or_else(|| t.surface_hover().alpha(if is_dark { 0.45 } else { 0.6 }));
963 let highlight_bg = t
964 .color(crate::theme::ColorName::Yellow, if is_dark { 7 } else { 2 })
965 .alpha(if is_dark { 0.45 } else { 0.7 });
966 let placeholder_color = style.placeholder.unwrap_or_else(|| t.dimmed().hsla());
967 let rule_color = t.border().hsla();
968 let quote_bar = t.primary().alpha(0.55);
969 let radius = t.radius(t.default_radius);
970 let token_colors: [Hsla; 8] = TokenKind::ALL.map(|kind| token_color(kind, t));
971
972 let prose = window.text_style().font();
973 let mono = Font {
974 family: MONO_FAMILY.into(),
975 ..prose.clone()
976 };
977 let cell_w = {
978 let ts = window.text_system();
979 let font_id = ts.resolve_font(&prose);
980 ts.ch_advance(font_id, px(base))
981 .map(f32::from)
982 .unwrap_or(base * 0.55)
983 };
984 self.cell_w = cell_w;
985 let base_line_h = (base * 1.6).round();
986
987 let cursor = self.model.cursor();
988 let selection = self.model.selection();
989 let reveal_range = match selection {
990 _ if !focused || self.read_only => None,
991 Some((s, e)) => Some((s.line, e.line)),
992 None => Some((cursor.line, cursor.line)),
993 };
994 let show_caret = focused && !self.read_only;
995 let show_placeholder = self.model.is_empty() && !focused && !self.placeholder.is_empty();
996
997 let wrap_total = self.wrap_w.max(120.0);
999 let mut rows: Vec<Row> = Vec::with_capacity(self.model.line_count());
1000 let mut doc_state = DocState::default();
1001 let mut hl_state = LineState::default();
1002 let mut y = 0.0;
1003 for (i, line) in self.model.lines().iter().enumerate() {
1004 let lang = doc_state.fence_lang().map(str::to_string);
1005 let block = classify(line, &mut doc_state);
1006 if matches!(block, Block::Fence { open: true, .. }) {
1007 hl_state = LineState::default();
1008 }
1009 let reveal = reveal_range.is_some_and(|(s, e)| i >= s && i <= e);
1010 let plan = plan(line, &block, lang.as_deref(), reveal);
1011
1012 let m = metrics(&plan.kind);
1013 let (scale, lh, pt, pb) = (m.scale, m.line_height, m.pad_top, m.pad_bottom);
1014 let size = (base * scale).round();
1015 let line_h = (size * lh).round();
1016 let pad_top = (base * pt).round();
1017 let pad_bottom = (base * pb).round();
1018
1019 let inset = match &plan.kind {
1020 RowKind::Bullet { cols } | RowKind::Task { cols, .. } => {
1021 *cols as f32 * cell_w + (base * 1.6).round()
1022 }
1023 RowKind::Ordered { cols, number } => {
1024 let shaped = window.text_system().shape_line(
1025 SharedString::from(format!("{number}.")),
1026 px(size),
1027 &[TextRun {
1028 len: format!("{number}.").len(),
1029 font: prose.clone(),
1030 color: Hsla::default(),
1031 background_color: None,
1032 underline: None,
1033 strikethrough: None,
1034 }],
1035 None,
1036 );
1037 *cols as f32 * cell_w + f32::from(shaped.width) + (base * 0.55).round()
1038 }
1039 RowKind::Quote { depth } => *depth as f32 * (base * 1.1).round(),
1040 RowKind::Code { .. } | RowKind::Fence { .. } => (base * 0.75).round(),
1041 _ => 0.0,
1042 };
1043 let right_pad = match &plan.kind {
1044 RowKind::Code { .. } | RowKind::Fence { .. } => inset,
1045 _ => 0.0,
1046 };
1047 let wrap = (wrap_total - inset - right_pad).max(60.0);
1048
1049 let runs = if let RowKind::Code { lang } = &plan.kind {
1050 let language = fence_language(lang.as_deref());
1051 let tokens = language.line(&plan.visible, &mut hl_state);
1052 cover(plan.visible.len(), &tokens)
1053 .into_iter()
1054 .map(|(len, kind)| TextRun {
1055 len,
1056 font: mono.clone(),
1057 color: kind.map_or(text_color, |k| token_colors[k.index()]),
1058 background_color: None,
1059 underline: None,
1060 strikethrough: None,
1061 })
1062 .collect::<Vec<_>>()
1063 } else {
1064 let heading = matches!(plan.kind, RowKind::Heading(_));
1065 plan.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)) =
1263 line_selection(start, end, i, text.chars().count())
1264 {
1265 let vs = vis_for_src(&row.plan.segs, byte_for_col(text, s_col));
1266 let ve = vis_for_src(&row.plan.segs, byte_for_col(text, e_col));
1267 for (x, vrow, w) in row.sel_rects(vs, ve, newline, cell_w) {
1268 el = el.child(
1269 div()
1270 .absolute()
1271 .left(px(row.inset + x))
1272 .top(px(row.pad_top + vrow as f32 * row.line_h))
1273 .w(px(w.max(2.0)))
1274 .h(px(row.line_h))
1275 .bg(selection_bg),
1276 );
1277 }
1278 }
1279 }
1280 }
1281
1282 if let Some(shaped) = row.text.clone() {
1285 if !row.plan.visible.is_empty() {
1286 let line_h = px(row.line_h);
1287 let text_h = row.visual_rows() as f32 * row.line_h;
1288 el = el.child(
1289 div()
1290 .absolute()
1291 .left(px(row.inset))
1292 .top(px(row.pad_top))
1293 .w(px((wrap_total - row.inset).max(60.0)))
1294 .h(px(text_h))
1295 .child(canvas(
1296 |_, _, _| (),
1297 move |bounds, _, window, cx| {
1298 let origin = bounds.origin;
1299 shaped
1300 .paint_background(
1301 origin,
1302 line_h,
1303 TextAlign::Left,
1304 None,
1305 window,
1306 cx,
1307 )
1308 .ok();
1309 shaped
1310 .paint(origin, line_h, TextAlign::Left, None, window, cx)
1311 .ok();
1312 },
1313 )),
1314 );
1315 }
1316 }
1317
1318 if show_caret && i == cursor.line {
1320 if let Some(text) = self.model.line(i) {
1321 let vis = vis_for_src(&row.plan.segs, byte_for_col(text, cursor.col));
1322 let (x, vrow) = row.caret(vis);
1323 el = el.child(
1324 div()
1325 .absolute()
1326 .left(px((row.inset + x - 1.0).max(0.0)))
1327 .top(px(row.pad_top + vrow as f32 * row.line_h))
1328 .w(px(2.0))
1329 .h(px(row.line_h))
1330 .bg(caret_color),
1331 );
1332 }
1333 }
1334
1335 row_divs.push(el);
1336 }
1337
1338 let entity = cx.entity();
1341 let probe = canvas(
1342 move |bounds, _window, cx| {
1343 entity.update(cx, |this, cx| {
1344 this.text_bounds = bounds;
1345 let w = f32::from(bounds.size.width);
1346 if (w - this.wrap_w).abs() > 0.5 {
1347 this.wrap_w = w;
1348 cx.notify();
1349 }
1350 });
1351 },
1352 |_, _, _, _| {},
1353 )
1354 .absolute()
1355 .size_full();
1356
1357 let mut lines_col = div()
1358 .relative()
1359 .flex()
1360 .flex_col()
1361 .w_full()
1362 .child(probe)
1363 .children(row_divs);
1364 if show_placeholder {
1365 lines_col = lines_col.child(
1366 div()
1367 .absolute()
1368 .top_0()
1369 .left_0()
1370 .text_color(placeholder_color)
1371 .child(self.placeholder.clone()),
1372 );
1373 }
1374
1375 let content = div().w_full().py(px(PAD_Y)).px(px(PAD_X)).child(lines_col);
1376
1377 let mut body = div()
1378 .id("guise-markdown-body")
1379 .track_focus(&self.focus)
1380 .on_key_down(cx.listener(Self::on_key))
1381 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1382 .on_drag(MarkdownDrag(cx.entity_id()), |_, _, _, cx| {
1383 cx.new(|_| Empty)
1384 })
1385 .on_drag_move(cx.listener(Self::on_drag_move))
1386 .overflow_y_scroll()
1387 .track_scroll(&self.scroll)
1388 .w_full()
1389 .max_h_full()
1390 .cursor_text()
1391 .child(content);
1392 if let Some(rows) = self.rows {
1393 body = body.min_h(px(rows as f32 * base_line_h + 2.0 * PAD_Y));
1394 }
1395
1396 let mut frame = div().flex().flex_col().w_full().h_full();
1397 if !style.bare {
1398 frame = frame
1399 .rounded(px(radius))
1400 .border_1()
1401 .border_color(frame_border);
1402 }
1403 frame
1404 .bg(bg)
1405 .overflow_hidden()
1406 .text_size(px(base))
1407 .line_height(px(base_line_h))
1408 .text_color(text_color)
1409 .child(body)
1410 .probe("MarkdownEditor")
1411 }
1412}
1413
1414fn fence_language(lang: Option<&str>) -> Language {
1418 match lang {
1419 Some("rust" | "rs") => Language::Rust,
1420 Some("sql") => Language::Sql,
1421 Some("json" | "jsonc") => Language::Json,
1422 _ => Language::None,
1423 }
1424}
1425
1426fn classify_alone(line: &str) -> Block {
1429 let mut state = DocState::default();
1430 classify("", &mut state);
1431 classify(line, &mut state)
1432}
1433
1434fn continuation(line: &str) -> Option<String> {
1438 match classify_alone(line) {
1439 Block::Task { indent, .. } => {
1440 let ch = line.as_bytes()[indent] as char;
1441 Some(format!("{ch} [ ] "))
1442 }
1443 Block::Bullet { indent, .. } => {
1444 let ch = line.as_bytes()[indent] as char;
1445 Some(format!("{ch} "))
1446 }
1447 Block::Ordered { indent, number, .. } => {
1448 let delim = line[indent..]
1449 .bytes()
1450 .find(|b| *b == b'.' || *b == b')')
1451 .unwrap_or(b'.') as char;
1452 Some(format!("{}{delim} ", number + 1))
1453 }
1454 Block::Quote { depth, .. } => Some("> ".repeat(depth as usize)),
1455 _ => None,
1456 }
1457}
1458
1459fn prefix_end(line: &str) -> usize {
1461 match classify_alone(line) {
1462 Block::Task { content, .. }
1463 | Block::Bullet { content, .. }
1464 | Block::Ordered { content, .. }
1465 | Block::Quote { content, .. } => content,
1466 _ => 0,
1467 }
1468}
1469
1470fn split_visual(bounds: &[usize], vs: usize, ve: usize) -> (usize, usize) {
1474 let sr = bounds.iter().filter(|&&b| b <= vs).count();
1475 let er = bounds.iter().filter(|&&b| b < ve).count();
1476 (sr, er.max(sr))
1477}
1478
1479fn cover(
1482 len: usize,
1483 tokens: &[(std::ops::Range<usize>, TokenKind)],
1484) -> Vec<(usize, Option<TokenKind>)> {
1485 let mut out = Vec::new();
1486 let mut at = 0;
1487 for (range, kind) in tokens {
1488 let start = range.start.max(at).min(len);
1489 let end = range.end.max(start).min(len);
1490 if start > at {
1491 out.push((start - at, None));
1492 }
1493 if end > start {
1494 out.push((end - start, Some(*kind)));
1495 }
1496 at = end.max(at);
1497 }
1498 if at < len {
1499 out.push((len - at, None));
1500 }
1501 out
1502}
1503
1504fn line_selection(
1508 start: Pos,
1509 end: Pos,
1510 line: usize,
1511 line_len: usize,
1512) -> Option<(usize, usize, bool)> {
1513 if line < start.line || line > end.line {
1514 return None;
1515 }
1516 let s = if line == start.line {
1517 start.col.min(line_len)
1518 } else {
1519 0
1520 };
1521 let e = if line == end.line {
1522 end.col.min(line_len)
1523 } else {
1524 line_len
1525 };
1526 let e = e.max(s);
1527 let newline = line < end.line;
1528 if e == s && !newline {
1529 return None;
1530 }
1531 Some((s, e, newline))
1532}
1533
1534fn scroll_adjust(offset: f32, view: f32, top: f32, bottom: f32) -> f32 {
1537 let mut adjusted = offset;
1538 if bottom + adjusted > view {
1539 adjusted = view - bottom;
1540 }
1541 if top + adjusted < 0.0 {
1542 adjusted = -top;
1543 }
1544 adjusted
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549 use super::*;
1550
1551 #[test]
1552 fn continuation_markers() {
1553 assert_eq!(continuation("- item"), Some("- ".into()));
1554 assert_eq!(continuation("* item"), Some("* ".into()));
1555 assert_eq!(continuation(" - item"), Some("- ".into()));
1556 assert_eq!(continuation("- [x] done"), Some("- [ ] ".into()));
1557 assert_eq!(continuation("3. third"), Some("4. ".into()));
1558 assert_eq!(continuation("3) third"), Some("4) ".into()));
1559 assert_eq!(continuation("> quoted"), Some("> ".into()));
1560 assert_eq!(continuation("> > deep"), Some("> > ".into()));
1561 assert_eq!(continuation("plain"), None);
1562 assert_eq!(continuation("# heading"), None);
1563 }
1564
1565 #[test]
1566 fn prefix_end_finds_content() {
1567 assert_eq!(prefix_end("- item"), 2);
1568 assert_eq!(prefix_end(" - [ ] x"), 8);
1569 assert_eq!(prefix_end("> q"), 2);
1570 assert_eq!(prefix_end("plain"), 0);
1571 }
1572
1573 #[test]
1574 fn split_visual_rows() {
1575 assert_eq!(split_visual(&[], 0, 10), (0, 0));
1577 assert_eq!(split_visual(&[10], 2, 8), (0, 0));
1579 assert_eq!(split_visual(&[10], 2, 15), (0, 1));
1580 assert_eq!(split_visual(&[10], 12, 15), (1, 1));
1581 assert_eq!(split_visual(&[10], 10, 15), (1, 1));
1583 assert_eq!(split_visual(&[10], 2, 10), (0, 0));
1584 assert_eq!(split_visual(&[10], 10, 10), (1, 1));
1586 }
1587
1588 #[test]
1589 fn cover_spans_exactly() {
1590 let tokens = vec![(2..5, TokenKind::Keyword)];
1591 let s = cover(10, &tokens);
1592 let total: usize = s.iter().map(|(len, _)| len).sum();
1593 assert_eq!(total, 10);
1594 assert_eq!(s[1], (3, Some(TokenKind::Keyword)));
1595 assert_eq!(cover(4, &[]), vec![(4, None)]);
1596 }
1597
1598 #[test]
1599 fn fence_language_mapping() {
1600 assert_eq!(fence_language(Some("rust")), Language::Rust);
1601 assert_eq!(fence_language(Some("rs")), Language::Rust);
1602 assert_eq!(fence_language(Some("json")), Language::Json);
1603 assert_eq!(fence_language(Some("python")), Language::None);
1604 assert_eq!(fence_language(None), Language::None);
1605 }
1606
1607 #[test]
1608 fn scroll_adjust_reveals_target() {
1609 assert_eq!(scroll_adjust(-10.0, 100.0, 20.0, 40.0), -10.0);
1610 assert_eq!(scroll_adjust(-50.0, 100.0, 20.0, 40.0), -20.0);
1611 assert_eq!(scroll_adjust(0.0, 100.0, 150.0, 170.0), -70.0);
1612 }
1613
1614 fn at(line: usize, col: usize) -> Pos {
1615 Pos::new(line, col)
1616 }
1617
1618 #[test]
1619 fn line_selection_matches_editor_semantics() {
1620 assert_eq!(
1621 line_selection(at(1, 2), at(1, 5), 1, 8),
1622 Some((2, 5, false))
1623 );
1624 assert_eq!(line_selection(at(0, 3), at(2, 2), 1, 4), Some((0, 4, true)));
1625 assert_eq!(line_selection(at(0, 3), at(2, 2), 3, 4), None);
1626 assert_eq!(line_selection(at(0, 0), at(2, 1), 1, 0), Some((0, 0, true)));
1627 }
1628}