Skip to content
Draft
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
25 changes: 21 additions & 4 deletions app/src/notebooks/editor/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub struct NotebooksEditorModel {
/// Context used to generate clickable file path links for notebooks.
file_link_resolution_context: Option<FileLinkResolutionContext>,
default_mermaid_display_mode: MarkdownDisplayMode,
lazy_layout: bool,
}

#[derive(Clone)]
Expand Down Expand Up @@ -239,6 +240,7 @@ impl NotebooksEditorModel {
resize_tx,
file_link_resolution_context: None,
default_mermaid_display_mode: MarkdownDisplayMode::Raw,
lazy_layout,
}
}

Expand Down Expand Up @@ -282,8 +284,19 @@ impl NotebooksEditorModel {
}

/// Set the window this editor model is associated with. Should be called when the pane attaches.
pub fn set_window_id(&mut self, window_id: WindowId, _ctx: &mut ModelContext<Self>) {
pub fn set_window_id(&mut self, window_id: WindowId, ctx: &mut ModelContext<Self>) {
self.rte_window_id = Some(window_id);
self.child_models.update(
self.interaction_state.clone(),
self.content.clone(),
self.selection_model.clone(),
window_id,
self.default_mermaid_display_mode,
ctx,
);
if self.sync_mermaid_render_offsets(ctx) && !self.lazy_layout {
self.rebuild_layout(ctx);
}
}

/// Get the context for the session and working directory associated with this editor, if any.
Expand Down Expand Up @@ -2181,9 +2194,13 @@ impl ChildModels {
.cloned()
}

/// Update the sub-model state with [`NotebookCommand`] models for every runnable command
/// in the buffer. This should be called after text layout completes, so that the offsets of
/// each block line up between the render and content models.
/// Update the sub-model state with [`NotebookCommand`] models for every runnable command in the
/// buffer.
///
/// Synchronization after text layout keeps edited render and content offsets aligned. A lazy
/// model may also update before its first layout once it is bound to a window: buffer outlines
/// already have stable content offsets, and the child models provide the rendered Mermaid
/// offsets that first layout consumes.
pub fn update<T: Entity>(
&mut self,
interaction_state: ModelHandle<InteractionStateModel>,
Expand Down
64 changes: 64 additions & 0 deletions app/src/notebooks/editor/model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,70 @@ fn command_range(
})
}

#[test]
fn test_lazy_model_initializes_rendered_mermaid_before_first_layout() {
App::test((), |mut app| async move {
initialize_deps(&mut app);
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
let _editable_flag = FeatureFlag::EditableMarkdownMermaid.override_enabled(true);
let window = setup_editor_window(&mut app, true);
let model = app.add_model(|ctx| {
let styles = rich_text_styles(Appearance::as_ref(ctx), FontSettings::as_ref(ctx));
let mut model = NotebooksEditorModel::new_unbound_lazy(styles, ctx);
model.set_default_mermaid_display_mode(MarkdownDisplayMode::Rendered, ctx);
model.reset_with_markdown("```mermaid\ngraph TD\nA --> B\n```", ctx);
model
});
layout_model(&mut app, &model).await;

model.update(&mut app, |model, ctx| model.set_window_id(window, ctx));

let commands = command_models(&model, &mut app);
let mermaid = commands
.into_iter()
.exactly_one()
.expect("expected one Mermaid command model");
let expected_range = command_range(&mermaid, &mut app);
let render_state = app.read(|ctx| model.as_ref(ctx).render_state().clone());
render_state.update(&mut app, |render_state, ctx| {
render_state.set_viewport_size(
SizeInfo {
viewport_size: Vector2F::new(800., 600.),
needs_layout: false,
},
ctx,
);
});
let pending_edits_flushed =
app.read(|ctx| render_state.as_ref(ctx).try_layout_pending_edits(ctx));
assert!(pending_edits_flushed);

app.read(|ctx| {
let model = model.as_ref(ctx);
let render_state = model.render_state().as_ref(ctx);
let content = render_state.content();
let mut offset = CharOffset::zero();
let (mermaid_offset, mermaid_item) = content
.block_items()
.find_map(|item| {
let item_offset = offset;
offset += item.content_length();
matches!(item, BlockItem::MermaidDiagram { .. }).then_some((item_offset, item))
})
.expect("first lazy layout should produce a Mermaid diagram");

assert_eq!(mermaid_offset, expected_range.start);
assert_eq!(
mermaid_item.content_length(),
expected_range.end - expected_range.start
);
assert_eq!(mermaid_item.lines(), 1.into());
assert!(mermaid_item.content_width().as_f32() > 0.);
assert!(mermaid_item.content_height().as_f32() > 0.);
});
});
}

/// Wait for text layout to finish.
async fn layout_model(app: &mut App, model: &ModelHandle<NotebooksEditorModel>) {
app.read(|ctx| model.as_ref(ctx).render_state.as_ref(ctx).layout_complete())
Expand Down
114 changes: 89 additions & 25 deletions crates/editor/src/content/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ pub fn resolve_asset_source_relative_to_directory(
}
}

#[derive(Clone, Copy)]
enum LayoutConcurrency {
Parallel,
#[cfg(any(target_os = "macos", test))]
Sequential,
}

/// Resolve an image source when its Markdown block is laid out.
///
/// Local-file metadata is read here so refreshes get a new cache key, while
Expand Down Expand Up @@ -578,6 +585,44 @@ impl EditDelta {
layout_options: &RenderLayoutOptions,
hidden_ranges: Option<RangeSet<CharOffset>>,
app: &AppContext,
) -> LaidOutRenderDelta {
self.layout_delta_with_concurrency(
layout,
document_path,
layout_options,
hidden_ranges,
app,
LayoutConcurrency::Parallel,
)
}

#[cfg(any(target_os = "macos", test))]
pub(crate) fn layout_delta_sequential(
&self,
layout: &TextLayout,
document_path: Option<&Path>,
layout_options: &RenderLayoutOptions,
hidden_ranges: Option<RangeSet<CharOffset>>,
app: &AppContext,
) -> LaidOutRenderDelta {
self.layout_delta_with_concurrency(
layout,
document_path,
layout_options,
hidden_ranges,
app,
LayoutConcurrency::Sequential,
)
}

fn layout_delta_with_concurrency(
&self,
layout: &TextLayout,
document_path: Option<&Path>,
layout_options: &RenderLayoutOptions,
hidden_ranges: Option<RangeSet<CharOffset>>,
app: &AppContext,
concurrency: LayoutConcurrency,
) -> LaidOutRenderDelta {
let hidden_ranges = hidden_ranges.unwrap_or_default();

Expand Down Expand Up @@ -623,34 +668,53 @@ impl EditDelta {

for chunk in chunk_layout_tasks(layout_tasks) {
let chunk_len = chunk.len();
let (chunk_items, chunk_trailing_newline): (Vec<_>, Last<_>) = chunk
.into_par_iter()
.enumerate()
.filter_map(|(local_idx, (task, is_hidden))| {
let idx = chunk_start + local_idx;
let location = if idx == 0 {
BlockLocation::Start
} else if idx >= last_task {
BlockLocation::End
} else {
BlockLocation::Middle
};

match task.run(layout, location, is_hidden) {
Ok(result) => Some(result),
Err(e) => {
report_error!(
e.context("Failed to lay out BlockItem"),
extra: { "offset" => ?self.old_offset }
);
None
}
let layout_task = |(local_idx, (task, is_hidden)): (usize, (LayoutTask<'_>, bool))| {
let idx = chunk_start + local_idx;
let location = if idx == 0 {
BlockLocation::Start
} else if idx >= last_task {
BlockLocation::End
} else {
BlockLocation::Middle
};

match task.run(layout, location, is_hidden) {
Ok(result) => Some(result),
Err(e) => {
report_error!(
e.context("Failed to lay out BlockItem"),
extra: { "offset" => ?self.old_offset }
);
None
}
})
.unzip();
}
};
let (chunk_items, chunk_trailing_newline) = match concurrency {
LayoutConcurrency::Parallel => {
let (items, trailing_newline): (Vec<_>, Last<_>) = chunk
.into_par_iter()
.enumerate()
.filter_map(layout_task)
.unzip();
(items, trailing_newline.into_inner())
}
#[cfg(any(target_os = "macos", test))]
LayoutConcurrency::Sequential => {
let results: Vec<_> = chunk
.into_iter()
.enumerate()
.filter_map(layout_task)
.collect();
let trailing_newline = results
.last()
.map(|(_, trailing_newline)| *trailing_newline);
let items = results.into_iter().map(|(item, _)| item).collect();
(items, trailing_newline)
}
};

block_items.extend(chunk_items);
if let Some(trailing_newline) = chunk_trailing_newline.into_inner() {
if let Some(trailing_newline) = chunk_trailing_newline {
has_trailing_newline = Some(trailing_newline);
}
chunk_start += chunk_len;
Expand Down
Loading
Loading