Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions crates/base/src/text/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,7 @@ pub(crate) struct NodeContext {
pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
pub(crate) table_actions: Option<Arc<TableActionsFn>>,
pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
/// This frame's streamed fade-in, when any text is still fading.
Expand All @@ -1947,6 +1948,13 @@ pub(crate) struct NodeContext {
}

impl NodeContext {
fn image_source(&self, image: &ImageNode) -> ImageSource {
match &self.image_source {
Some(resolve) => resolve(&image.url),
None => image.source(),
}
}

pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
self.link_refs.insert(identifier, link);
}
Expand Down Expand Up @@ -2143,7 +2151,7 @@ impl Paragraph {
}
let link_click_handler = node_cx.link_click_handler.clone();
child_nodes.push(
img(image.source())
img(node_cx.image_source(image))
.id(ix)
.object_fit(ObjectFit::Contain)
.max_w(relative(1.))
Expand Down Expand Up @@ -2360,7 +2368,7 @@ impl Paragraph {
}

items.push(InlineFlowItem::Image {
source: image.source(),
source: node_cx.image_source(image),
link: image.link.clone(),
title: image.title(),
width: image.width,
Expand Down
3 changes: 3 additions & 0 deletions crates/base/src/text/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub struct TextViewState {
pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
pub(super) code_block_highlighter: Option<std::sync::Arc<CodeBlockHighlighterFn>>,
pub(super) table_actions: Option<std::sync::Arc<TableActionsFn>>,
pub(super) image_source: Option<std::sync::Arc<super::text_view::ImageSourceFn>>,
pub(super) link_click_handler: Option<std::sync::Arc<LinkClickHandlerFn>>,
pub(super) markdown_extensions: Arc<MarkdownExtensions>,

Expand Down Expand Up @@ -247,6 +248,7 @@ impl TextViewState {
code_block_highlighter: None,
table_actions: None,
link_click_handler: None,
image_source: None,
markdown_extensions: Arc::default(),
is_selecting: false,
preserve_inline_selection: false,
Expand Down Expand Up @@ -987,6 +989,7 @@ impl Render for TextViewState {
code_block_highlighter: self.code_block_highlighter.clone(),
table_actions: self.table_actions.clone(),
link_click_handler: self.link_click_handler.clone(),
image_source: self.image_source.clone(),
markdown_extensions: self.markdown_extensions.clone(),
stream_fade,
range_highlights: self.range_highlights.clone(),
Expand Down
20 changes: 20 additions & 0 deletions crates/base/src/text/text_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ impl TextViewDefaults {
pub(crate) type TableActionsFn =
dyn Fn(&TableData, &mut Window, &mut App) -> AnyElement + Send + Sync;

pub(crate) type ImageSourceFn = dyn Fn(&gpui::SharedUri) -> gpui::ImageSource + Send + Sync;

pub(crate) type LinkClickHandlerFn =
dyn Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync;

Expand Down Expand Up @@ -134,6 +136,7 @@ pub struct TextView {
code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
table_actions: Option<Arc<TableActionsFn>>,
link_click_handler: Option<Arc<LinkClickHandlerFn>>,
image_source: Option<Arc<ImageSourceFn>>,
reveal_handler: Option<Rc<RevealHandlerFn>>,
markdown_extensions: Arc<MarkdownExtensions>,
motion: Option<TextViewMotion>,
Expand Down Expand Up @@ -180,6 +183,7 @@ impl TextView {
code_block_highlighter: None,
table_actions: None,
link_click_handler: None,
image_source: None,
reveal_handler: None,
markdown_extensions: Arc::default(),
motion: None,
Expand All @@ -203,6 +207,7 @@ impl TextView {
code_block_highlighter: None,
table_actions: None,
link_click_handler: None,
image_source: None,
reveal_handler: None,
markdown_extensions: Arc::default(),
motion: None,
Expand All @@ -226,12 +231,26 @@ impl TextView {
code_block_highlighter: None,
table_actions: None,
link_click_handler: None,
image_source: None,
reveal_handler: None,
markdown_extensions: Arc::default(),
motion: None,
}
}

/// Overrides the source of every document image, including embedded data URLs.
///
/// Used for both rendering and intrinsic-size measurement. The returned source
/// is authoritative: pending or failed loads never fall back to the document URL.
/// Without this override, images use Base's default URI and data URL handling.
pub fn image_source<F>(mut self, resolver: F) -> Self
where
F: Fn(&gpui::SharedUri) -> gpui::ImageSource + Send + Sync + 'static,
{
self.image_source = Some(Arc::new(resolver));
self
}

/// Set [`TextViewStyle`].
pub fn style(mut self, style: TextViewStyle) -> Self {
self.text_view_style = Some(style);
Expand Down Expand Up @@ -604,6 +623,7 @@ impl Element for TextView {
state.code_block_highlighter = code_block_highlighter;
state.table_actions = self.table_actions.clone();
state.link_click_handler = self.link_click_handler.clone();
state.image_source = self.image_source.clone();
state.set_markdown_extensions(self.markdown_extensions.clone(), cx);
if let Some(motion) = &self.motion {
state.set_motion(motion.clone());
Expand Down
6 changes: 6 additions & 0 deletions crates/shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,17 @@ tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots", "url"],

anyhow.workspace = true
instant.workspace = true
# Decode only bytes fetched through the document's network grant. Codec features
# are shared with GPUI, whose image loader previously decoded these documents.
image = { version = "0.25", default-features = false }
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
# Only to find the files an SVG image references; GPUI's version, which renders it.
usvg = { version = "0.46", default-features = false }
tracing.workspace = true

[dev-dependencies]
Expand Down
12 changes: 12 additions & 0 deletions crates/shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,18 @@ limits. Every redirect target must be granted; HTTPS downgrade is refused, as
are cross-origin POST replays and cross-origin redirects carrying Authorization
or any caller-supplied header.

Images in `TextView.html` and `TextView.markdown` use the document's captured
network grant, including inline images and intrinsic-size measurement. Only
absolute HTTP(S) URLs authorized for GET can load; relative, scheme-less,
`data:`, `file:`, custom-scheme and credential-bearing URLs are refused, as
is an SVG image whose `<image>` references a file. Each
redirect is re-authorized, with at most 10 redirects and no HTTPS downgrade.
Requests have a 30-second timeout and an 8 MiB response limit. Image loading
never falls back to the host's unrestricted URI loader. Each TextView and
policy identity has its own cache, released with its native element state;
a broader grant cannot populate a cache used by a narrower grant. The ordinary
application-asset `image(path)` API and default link handling are unchanged.

Import `WebSocket` from `websocket`; `WebSocket.connect(url, { headers })` resolves after the handshake and returns
async `read`, `write`, and `close` methods for text and binary messages. Frames
and messages are limited to 8 MiB. Connect/handshake and writes have 30-second
Expand Down
13 changes: 8 additions & 5 deletions crates/shell/src/engine/quickjs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7147,11 +7147,14 @@ impl ShellRuntime {
"markdown" => crate::spec::TextViewFormat::Markdown,
_ => return Err(Exception::throw_type(&ctx, "TextView format must be html or markdown")),
};
Ok(upgrade(&text_view_runtime, &ctx)?.push_node(Component::TextView {
id: id.into(),
text: text.into(),
format,
}))
Ok(upgrade(&text_view_runtime, &ctx)?.push_node(Component::TextView(
crate::spec::TextViewSpec {
id: id.into(),
text: text.into(),
format,
policy: crate::scope::policy(),
},
)))
}),
)?;
text_constructor(&globals, "__svg", runtime.clone(), Component::Svg)?;
Expand Down
11 changes: 6 additions & 5 deletions crates/shell/src/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ use gpui_base::{
};

mod components;
mod text_view;

use crate::{
capability::is_openable_url,
Expand Down Expand Up @@ -1059,10 +1060,10 @@ fn materialize_component(
cx,
)
}
Component::TextView { id, text, format } => {
let mut view = match format {
crate::spec::TextViewFormat::Html => TextView::html(id, text),
crate::spec::TextViewFormat::Markdown => TextView::markdown(id, text),
Component::TextView(spec) => {
let mut view = match spec.format {
crate::spec::TextViewFormat::Html => TextView::html(spec.id, spec.text),
crate::spec::TextViewFormat::Markdown => TextView::markdown(spec.id, spec.text),
}
.style(TextViewStyle::from_theme(&Theme::global(cx)));
if let Some(selectable) = behavior.selectable {
Expand Down Expand Up @@ -1095,7 +1096,7 @@ fn materialize_component(
});
}
Styled::style(&mut view).refine(&refinement);
view.into_any_element()
text_view::with_policy(view, spec.policy).into_any_element()
}
Component::Text(value) => {
// A text run, not a `div` holding one. GPUI implements
Expand Down
Loading
Loading