feat: add markdown view component

This commit is contained in:
2026-11-06 17:04:56 +07:00
parent e8e1cb39e0
commit 310cad32be
9 changed files with 1957 additions and 13 deletions
+167 -1
View File
@@ -43,6 +43,7 @@ use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_editor::code_view::CodeViewState;
use gpui_native_editor::diff_view::{DiffRowKind, DiffViewState};
use gpui_native_editor::highlight::Language;
use gpui_native_editor::markdown::{MarkdownBlock, MarkdownViewState};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, DockSidebar, DockState, PanelDescriptor, SplitDirection};
use gpui_native_layout::resize_handle::ResizeHandle;
@@ -87,7 +88,11 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
),
(
"Editor",
&[("code-view", "Code view"), ("diff-view", "Diff view")],
&[
("code-view", "Code view"),
("diff-view", "Diff view"),
("markdown-view", "Markdown view"),
],
),
(
"Feedback",
@@ -642,6 +647,12 @@ impl Gallery {
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("markdown-view", "Markdown view", |_, cx| {
cx.new(MarkdownViewDemo::new).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
cx.new(|_| SidebarDemo { selected: None }).into()
@@ -3102,6 +3113,161 @@ impl Render for DiffViewDemo {
}
}
/// The markdown view demo's sample document.
const MARKDOWN_SAMPLE: &str = r###"# Release notes
Everything shipped this week, with **bold claims**, *quiet fixes*, and ~~regressions~~.
## Highlights
- The relay pool now [retries automatically](https://example.com/relay) on stale sockets
- Inline `code spans` and keyboard chords read fine
### Checklist
- [x] Ship the diff view
- [x] Ship the markdown view
- [ ] Wire it into signed
- [ ] Profile the large file
1. Cut the branch
2. Run the gallery
1. Open this panel
2. Scroll the large file
> Notes from the field: **measure first**, then optimize.
```rust
fn bind_all(&self) -> Result<(), String> {
for port in &self.ports {
if port > &1024 {
continue;
}
return Err(format!("port {} is privileged", port));
}
Ok(())
}
```
| Area | Status | Owner |
|------|--------|-------|
| Editor | shipping | reya |
| Layout | staged | team |
---
That is the news.
"###;
/// The section count of the large markdown sample.
const LARGE_MARKDOWN_SECTIONS: usize = 1000;
/// Generates a long markdown document, so scrolling the view can be
/// exercised on realistic content.
fn large_markdown(sections: usize) -> String {
let mut source = String::new();
for section in 0..sections {
source.push_str(&format!("## Section {section}\n\n"));
source.push_str(&format!(
"Handler {section} retries stale sockets and logs every outcome.\n\n"
));
source.push_str(&format!(
"- [ ] Audit handler {section}\n- See [issue {section}](https://example.com/issues/{section})\n\n"
));
source.push_str("```rust\nfn handle() -> bool { true }\n```\n\n");
}
source
}
struct MarkdownViewDemo {
view: Entity<MarkdownViewState>,
sample: MarkdownSample,
large_source: SharedString,
_subscription: gpui::Subscription,
}
/// The markdown view demo's sample choice.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum MarkdownSample {
#[default]
Guide,
/// A generated document with thousands of blocks, to test scroll
/// performance.
Large,
}
impl MarkdownViewDemo {
fn new(cx: &mut Context<Self>) -> Self {
let view = cx.new(|_| MarkdownViewState::new("markdown-view-demo", MARKDOWN_SAMPLE));
// The panel is a cached view in the dock: it re-renders only when
// the markdown state it renders is notified.
let subscription = cx.observe(&view, |_, _, cx| cx.notify());
Self {
view,
sample: MarkdownSample::Guide,
large_source: large_markdown(LARGE_MARKDOWN_SECTIONS).into(),
_subscription: subscription,
}
}
}
impl Render for MarkdownViewDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let radius = cx.theme().radius;
let view = cx.entity();
let blocks = self.view.read(cx).blocks();
let block_count = blocks.len();
let heading_count = blocks
.iter()
.filter(|block| matches!(block, MarkdownBlock::Heading { .. }))
.count();
ScrollArea::new("markdown-view-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"document",
colors,
Segmented::new("markdown-view-demo-sample", self.sample)
.options(vec![
(MarkdownSample::Guide, "Release notes"),
(MarkdownSample::Large, "Large file"),
])
.on_change(move |next, _, cx| {
view.update(cx, |demo, cx| {
demo.sample = next;
let source = match next {
MarkdownSample::Guide => MARKDOWN_SAMPLE.into(),
MarkdownSample::Large => demo.large_source.clone(),
};
demo.view.update(cx, |view, cx| view.set_source(source, cx));
});
}),
))
.child(demo_section(
"rendered, virtualized, links clickable",
colors,
div()
.h(px(480.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(self.view.clone()),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"{block_count} blocks, {heading_count} headings \u{2014} scroll to test performance"
)),
),
)
}
}
struct SidebarDemo {
selected: Option<usize>,
}