use std::any::Any; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use pathfinder_geometry::vector::{Vector2F, vec2f}; use serde_yaml::Mapping; use uuid::Uuid; use warp_editor::content::markdown::MarkdownStyle; use warp_editor::editor::EmbeddedItemModel; use warp_editor::render::element::{RenderContext, RenderableBlock}; use warp_editor::render::layout::TextLayout; use warp_editor::render::model::viewport::ViewportItem; use warp_editor::render::model::{ BlockSpacing, EmbeddedItem, EmbeddedItemHTMLRepresentation, EmbeddedItemRichFormat, LaidOutEmbeddedItem, RenderState, }; use warp_errors::report_error; use warpui::event::DispatchedEvent; use warpui::units::Pixels; use warpui::{AppContext, EntityId, EventContext, LayoutContext, ViewHandle, WindowId}; use crate::code::editor::comment_editor::CommentEditor; use crate::code_review::comments::CommentId; const COMMENT_ID_MAPPING_KEY: &str = "comment_id"; const ENTITY_ID_MAPPING_KEY: &str = "entity_id"; const WINDOW_ID_MAPPING_KEY: &str = "window_id"; #[derive(Debug)] pub struct EmbeddedCommentSpace { // We unfortunately need to store a string version of the ID // in order to return it in EmbeddedItem::hashed_id() id_string: String, editor_entity_id: EntityId, window_id: WindowId, } impl EmbeddedCommentSpace { fn new(id: CommentId, editor_entity_id: EntityId, window_id: WindowId) -> Self { Self { id_string: id.to_string(), editor_entity_id, window_id, } } // Fetch the underlying comment editor view fn get_comment_editor(&self, app: &AppContext) -> Option> { app.view_with_id::(self.window_id, self.editor_entity_id) } } impl EmbeddedItem for EmbeddedCommentSpace { fn layout(&self, _text_layout: &TextLayout, app: &AppContext) -> Box { let comment_editor = self.get_comment_editor(app); if comment_editor.is_none() { report_error!( "EmbeddedComment can't layout missing comment editor", extra: { "comment_id" => ?self.id_string } ); }; let size = comment_editor .and_then(|editor| editor.read(app, |editor, _ctx| editor.get_laid_out_size())) .unwrap_or_else(|| { report_error!( "Didn't find laid out size for editor", extra: { "editor_id" => ?self.id_string } ); Vector2F::new(100.0, 24.0) }); Box::new(LaidOutEmbeddedCommentSpace { size }) } fn hashed_id(&self) -> &str { self.id_string.as_str() } fn to_mapping(&self, _style: MarkdownStyle) -> Mapping { let mut map = Mapping::new(); let comment_id = self.id_string.clone(); let editor_entity_id = self.editor_entity_id.to_string(); let window_id = self.window_id.to_string(); map.insert(COMMENT_ID_MAPPING_KEY.into(), comment_id.into()); map.insert(ENTITY_ID_MAPPING_KEY.into(), editor_entity_id.into()); map.insert(WINDOW_ID_MAPPING_KEY.into(), window_id.into()); map } fn to_rich_format(&self, app: &AppContext) -> EmbeddedItemRichFormat<'_> { let text = match self.get_comment_editor(app) { Some(editor) => editor.read(app, |editor, app| editor.comment_text(app)), _ => String::new(), }; EmbeddedItemRichFormat { plain_text: text.to_string(), html: EmbeddedItemHTMLRepresentation { element_name: "div", content: text.to_string(), attributes: HashMap::new(), }, } } } #[derive(Debug)] pub struct LaidOutEmbeddedCommentSpace { pub size: Vector2F, } impl LaidOutEmbeddedItem for LaidOutEmbeddedCommentSpace { fn height(&self) -> Pixels { Pixels::new(self.size.y()) } fn size(&self) -> Vector2F { self.size } fn first_line_bound(&self) -> Vector2F { vec2f(self.size.x(), 24.0) } fn element( &self, _state: &RenderState, viewport_item: ViewportItem, _model: Option<&dyn EmbeddedItemModel>, _ctx: &AppContext, ) -> Box { // Just create a spacer - no child view rendering here Box::new(RenderableEmbeddedCommentSpace::new(viewport_item)) } fn spacing(&self) -> BlockSpacing { BlockSpacing::default() } fn as_any(&self) -> &dyn Any { self } } pub struct RenderableEmbeddedCommentSpace { viewport_item: ViewportItem, } impl RenderableEmbeddedCommentSpace { pub(crate) fn new(viewport_item: ViewportItem) -> Self { Self { viewport_item } } } impl RenderableBlock for RenderableEmbeddedCommentSpace { fn viewport_item(&self) -> &ViewportItem { &self.viewport_item } fn layout(&mut self, _model: &RenderState, _ctx: &mut LayoutContext, _app: &AppContext) { // No-op: this is just a spacer, the actual editor is laid out by EditorWrapper } fn paint(&mut self, _model: &RenderState, _ctx: &mut RenderContext, _app: &AppContext) { // No-op: this is just empty space, the actual editor is painted by EditorWrapper } fn dispatch_event( &mut self, _model: &RenderState, _event: &DispatchedEvent, _ctx: &mut EventContext, _app: &AppContext, ) -> bool { // No interactivity: events are handled by the editor rendered by EditorWrapper false } fn is_embedded_comment(&self) -> bool { true } } /// The embedded item transformation for comments. #[cfg_attr(not(test), allow(unused))] // TODO(CODE-1464): use this pub(super) fn comment_embedded_item_conversion( mut mapping: serde_yaml::Mapping, ) -> Option> { use serde_yaml::Value; let Some(Value::String(comment_uuid)) = mapping.remove(&Value::String(COMMENT_ID_MAPPING_KEY.to_string())) else { report_error!("Unable to deserialize embedded comment ID"); return None; }; let Some(Value::String(entity_id)) = mapping.remove(&Value::String(ENTITY_ID_MAPPING_KEY.to_string())) else { report_error!("Unable to deserialize embedded comment entity ID"); return None; }; let Some(Value::String(window_id)) = mapping.remove(&Value::String(WINDOW_ID_MAPPING_KEY.to_string())) else { report_error!("Unable to deserialize embedded comment window ID"); return None; }; let comment_id = CommentId::from_uuid( Uuid::from_str(&comment_uuid) .map_err(|e| { report_error!( anyhow::Error::new(e).context("Unable to parse comment ID"), extra: { "comment_id" => %comment_uuid } ); }) .ok()?, ); let entity_id = EntityId::from_usize( entity_id .parse::() .map_err(|e| { report_error!( anyhow::Error::new(e).context("Unable to parse entity ID"), extra: { "entity_id" => %entity_id } ); }) .ok()?, ); let window_id = WindowId::from_usize( window_id .parse::() .map_err(|e| { report_error!( anyhow::Error::new(e).context("Unable to parse window ID"), extra: { "window_id" => %window_id } ); }) .ok()?, ); Some(Arc::new(EmbeddedCommentSpace::new( comment_id, entity_id, window_id, ))) } #[cfg(test)] #[path = "embedded_comment_tests.rs"] mod tests;