#[cfg(not(target_family = "wasm"))]use std::sync::Arc; use ai::diff_validation::DiffType;#[cfg(not(target_family = "wasm"))]use futures::FutureExt;#[cfg(not(target_family = "wasm"))]use warp_files::{FileModel, FileModelEvent};use warp_util::file::FileId;#[cfg(not(target_family = "wasm"))]use warp_util::file::FileSaveError;use warp_util::standardized_path::StandardizedPath;#[cfg(not(target_family = "wasm"))]use warpui::SingletonEntity;use warpui::elements::ChildView;use warpui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle}; use super::diff_viewer::{DiffViewer, DisplayMode};use super::editor::NavBarBehavior;use super::editor::scroll::{ScrollPosition, ScrollTrigger};use super::editor::view::{CodeEditorEvent, CodeEditorView};use crate::ai::blocklist::diff_storage::SaveFuture;#[cfg(not(target_family = "wasm"))]use crate::ai::blocklist::diff_types::DiffSessionType;use crate::editor::InteractionState; pub enum InlineDiffViewEvent { DiffStatusUpdated, #[cfg(not(target_family = "wasm"))] FileLoaded, #[cfg(not(target_family = "wasm"))] FileSaved, #[cfg(not(target_family = "wasm"))] FailedToSave { error: Arc<FileSaveError>, }, UserEdited,} /// An inline diff viewer with optional file-backed save support.////// When a backing file is registered (via [`Self::register_file`]), this view supports the full/// accept/save/revert lifecycle through `FileModel`. Without a registered file, it behaves/// as a read-only diff viewer (e.g. for WASM or restored conversations).pub struct InlineDiffView { editor: ViewHandle<CodeEditorView>, diff_type: Option<DiffType>, file_path: Option<StandardizedPath>, /// Whether the user has edited the diff content. was_edited: bool, /// `FileModel` file ID for the backing file. Set via [`Self::register_file`]. /// /// When `Some`: /// - The editor is editable (interaction state follows the `DisplayMode` rules). /// - Accept, save, and revert operations write through `FileModel`. /// /// When `None` (WASM, restored conversations, or before registration): /// - The editor is selection-only (never editable). /// - Accept, save, and revert are no-ops. backing_file_id: Option<FileId>, /// Whether the diff is a new file creation (for revert: delete instead of restore). #[cfg(not(target_family = "wasm"))] is_new_file: bool,} impl InlineDiffView { pub fn new( editor: ViewHandle<CodeEditorView>, diff_type: Option<DiffType>, display_mode: Option<DisplayMode>, file_path: Option<StandardizedPath>, ctx: &mut ViewContext<Self>, ) -> Self { #[cfg(not(target_family = "wasm"))] let is_new_file = matches!(diff_type, Some(DiffType::Create { .. })); ctx.subscribe_to_view(&editor, |me, _view, event, ctx| match event { CodeEditorEvent::DiffUpdated => { ctx.emit(InlineDiffViewEvent::DiffStatusUpdated); } CodeEditorEvent::ContentChanged { origin } => { if origin.from_user() && !me.was_edited { me.was_edited = true; ctx.emit(InlineDiffViewEvent::UserEdited); } } _ => {} }); let model = Self { editor, diff_type, file_path, was_edited: false, backing_file_id: None, #[cfg(not(target_family = "wasm"))] is_new_file, }; model.apply_diffs_if_any(ctx); if let Some(display_mode) = display_mode { model.set_display_mode(display_mode, ctx); } model } /// Register a file with `FileModel` for save support. /// /// The `session_type` determines whether the file is local or remote. /// For `Local`, the file is registered by path on the local filesystem. /// For `Remote`, the file is registered against the remote backend so /// that `save()` / `delete()` dispatch over the wire via /// `RemoteServerClient`. /// /// This must be called after construction for non-WASM environments. #[cfg(not(target_family = "wasm"))] pub fn register_file(&mut self, session_type: &DiffSessionType, ctx: &mut ViewContext<Self>) { let Some(file_path) = &self.file_path else { return; }; let file_model = FileModel::handle(ctx); let file_id = match session_type { DiffSessionType::Local => { let Some(local_path) = file_path.to_local_path() else { crate::safe_error!( safe: ( "Failed to convert StandardizedPath to local path; diff will be \ read-only" ), full: ( "Failed to convert StandardizedPath to local path: {file_path}; diff \ will be read-only" ) ); return; }; file_model.update(ctx, |file_model, ctx| { file_model.register_file_path(&local_path, false, ctx) }) } DiffSessionType::Remote(host_id) => { let host_id = host_id.clone(); let remote_path = file_path.clone(); file_model.update(ctx, |file_model, _ctx| { file_model.register_remote_file(host_id, remote_path) }) } }; self.finish_file_registration(file_id, ctx); } /// Common registration logic: subscribes to events and sets the /// backing file ID after a file has been registered with `FileModel`. #[cfg(not(target_family = "wasm"))] fn finish_file_registration(&mut self, file_id: FileId, ctx: &mut ViewContext<Self>) { let file_model = FileModel::handle(ctx); let version = self.editor.as_ref(ctx).version(ctx); file_model.update(ctx, |file_model, _ctx| { file_model.set_version(file_id, version); }); self.backing_file_id = Some(file_id); // Subscribe to FileModel events for this file. ctx.subscribe_to_model(&file_model, move |_me, _file_model, event, ctx| { if file_id == event.file_id() { match event { FileModelEvent::FileSaved { .. } => { ctx.emit(InlineDiffViewEvent::FileSaved); } FileModelEvent::FailedToSave { error, .. } => { ctx.emit(InlineDiffViewEvent::FailedToSave { error: error.clone(), }); } _ => {} } } }); ctx.emit(InlineDiffViewEvent::FileLoaded); } fn apply_diffs_if_any(&self, ctx: &mut ViewContext<Self>) { let Some(diff) = self.diff_type.clone() else { return; }; let deltas = match diff { DiffType::Create { delta } => vec![delta], DiffType::Update { mut deltas, .. } => { deltas.sort_by_key(|delta| delta.replacement_line_range.start); deltas } DiffType::Delete { delta } => vec![delta], }; if deltas.is_empty() { return; } self.editor.update(ctx, |editor, ctx| { editor.apply_diffs(deltas, ctx); editor.toggle_diff_nav(None, ctx); editor.set_pending_scroll(ScrollTrigger::new( ScrollPosition::FocusedDiffHunk, editor.buffer_version(ctx), )); }); } /// Saves the current editor content through `FileModel`, returning the /// save's completion future. `FileSaved` / `FailedToSave` events still /// fire alongside. `None` when no file is registered. #[cfg(not(target_family = "wasm"))] fn save_content(&self, ctx: &mut ViewContext<Self>) -> Option<SaveFuture> { let file_id = self.backing_file_id?; let content = self.editor.as_ref(ctx).text(ctx).into_string(); let version = self.editor.as_ref(ctx).version(ctx); match FileModel::handle(ctx).update(ctx, |file_model, ctx| { file_model.save(file_id, content, version, ctx) }) { Ok(save_future) => Some(save_future), Err(err) => { let error = Arc::new(err); ctx.emit(InlineDiffViewEvent::FailedToSave { error: error.clone(), }); Some(futures::future::ready(Err(error)).boxed()) } } } /// Accepts the diff by saving the editor content to the backing file, /// returning the save's completion future. `None` when no file is /// registered (WASM / restored conversations), in which case nothing is /// dispatched. pub fn accept_and_save_diff(&self, ctx: &mut ViewContext<Self>) -> Option<SaveFuture> { #[cfg(not(target_family = "wasm"))] { self.save_content(ctx) } #[cfg(target_family = "wasm")] { let _ = ctx; None } }} impl InlineDiffView { pub fn file_path(&self) -> Option<&StandardizedPath> { self.file_path.as_ref() } /// Base content of the diff (from the editor's diff model), if set. pub fn base_content(&self, app: &AppContext) -> Option<String> { self.editor .as_ref(app) .model .as_ref(app) .diff() .as_ref(app) .base() .map(|base| base.to_string()) } pub fn file_name(&self) -> Option<String> { self.file_path() .map(|p| p.file_name().unwrap_or_default().to_owned()) }} impl DiffViewer for InlineDiffView { fn editor(&self) -> &ViewHandle<CodeEditorView> { &self.editor } fn diff(&self) -> Option<&DiffType> { self.diff_type.as_ref() } fn was_edited(&self) -> bool { self.was_edited } fn set_display_mode(&self, mode: DisplayMode, ctx: &mut ViewContext<Self>) { let is_delete = matches!(self.diff(), Some(DiffType::Delete { .. })); let interaction_state = if self.backing_file_id.is_some() { mode.interaction_state(is_delete) } else { // No file registered (e.g. WASM or restored conversations): always read-only. InteractionState::Selectable }; self.editor().update(ctx, |editor, ctx| { editor.set_scroll_wheel_behavior(mode.scroll_wheel_behavior()); editor.set_vertical_expansion_behavior(mode.vertical_expansion_behavior(), ctx); editor.set_vertical_scrollbar_appearance(mode.scrollbar_appearance()); editor.set_horizontal_scrollbar_appearance(mode.scrollbar_appearance()); editor.set_interaction_state(interaction_state, ctx); editor.set_show_nav_bar(mode.show_nav_bar()); editor.set_nav_bar_behavior(NavBarBehavior::NotClosable, ctx); }); } fn restore_diff_base(&mut self, _ctx: &mut ViewContext<Self>) -> Result<(), String> { // No-op when no file is registered (WASM / restored conversations). if self.backing_file_id.is_none() { return Ok(()); } #[cfg(not(target_family = "wasm"))] { let file_id = self .backing_file_id .expect("backing_file_id must be Some — checked by early return above"); if self.is_new_file { // For newly created files, delete instead of restoring. The // delete's completion is observed via `FileSaved` events. let version = self.editor.as_ref(_ctx).version(_ctx); FileModel::handle(_ctx) .update(_ctx, |file_model, ctx| { file_model.delete(file_id, version, ctx) }) .map(drop) .map_err(|e| format!("Failed to delete file: {e:?}"))?; return Ok(()); } // For existing files, restore the base content from the editor's DiffModel. let base_content = self .editor .as_ref(_ctx) .model .as_ref(_ctx) .diff() .as_ref(_ctx) .base() .ok_or_else(|| "Missing base content".to_string())? .to_string(); let version = self.editor.as_ref(_ctx).version(_ctx); // The revert save's completion is observed via `FileSaved` events. FileModel::handle(_ctx) .update(_ctx, |file_model, ctx| { file_model.save(file_id, base_content, version, ctx) }) .map(drop) .map_err(|e| format!("Failed to save file: {e:?}"))?; } Ok(()) }} impl Entity for InlineDiffView { type Event = InlineDiffViewEvent;} impl View for InlineDiffView { fn ui_name() -> &'static str { "InlineDiffView" } fn render(&self, _app: &AppContext) -> Box<dyn Element> { ChildView::new(&self.editor).finish() }} impl TypedActionView for InlineDiffView { type Action = ();}
#[cfg(not(target_family = "wasm"))]
use std::sync::Arc;
use ai::diff_validation::DiffType;
#[cfg(not(target_family = "wasm"))]
use futures::FutureExt;
#[cfg(not(target_family = "wasm"))]
use warp_files::{FileModel, FileModelEvent};
use warp_util::file::FileId;
#[cfg(not(target_family = "wasm"))]
use warp_util::file::FileSaveError;
use warp_util::standardized_path::StandardizedPath;
#[cfg(not(target_family = "wasm"))]
use warpui::SingletonEntity;
use warpui::elements::ChildView;
use warpui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
use super::diff_viewer::{DiffViewer, DisplayMode};
use super::editor::NavBarBehavior;
use super::editor::scroll::{ScrollPosition, ScrollTrigger};
use super::editor::view::{CodeEditorEvent, CodeEditorView};
use crate::ai::blocklist::diff_storage::SaveFuture;
#[cfg(not(target_family = "wasm"))]
use crate::ai::blocklist::diff_types::DiffSessionType;
use crate::editor::InteractionState;
pub enum InlineDiffViewEvent {
DiffStatusUpdated,
#[cfg(not(target_family = "wasm"))]
FileLoaded,
#[cfg(not(target_family = "wasm"))]
FileSaved,
#[cfg(not(target_family = "wasm"))]
FailedToSave {
error: Arc<FileSaveError>,
},
UserEdited,
}
/// An inline diff viewer with optional file-backed save support.
///
/// When a backing file is registered (via [`Self::register_file`]), this view supports the full
/// accept/save/revert lifecycle through `FileModel`. Without a registered file, it behaves
/// as a read-only diff viewer (e.g. for WASM or restored conversations).
pub struct InlineDiffView {
editor: ViewHandle<CodeEditorView>,
diff_type: Option<DiffType>,
file_path: Option<StandardizedPath>,
/// Whether the user has edited the diff content.
was_edited: bool,
/// `FileModel` file ID for the backing file. Set via [`Self::register_file`].
///
/// When `Some`:
/// - The editor is editable (interaction state follows the `DisplayMode` rules).
/// - Accept, save, and revert operations write through `FileModel`.
///
/// When `None` (WASM, restored conversations, or before registration):
/// - The editor is selection-only (never editable).
/// - Accept, save, and revert are no-ops.
backing_file_id: Option<FileId>,
/// Whether the diff is a new file creation (for revert: delete instead of restore).
#[cfg(not(target_family = "wasm"))]
is_new_file: bool,
}
impl InlineDiffView {
pub fn new(
editor: ViewHandle<CodeEditorView>,
diff_type: Option<DiffType>,
display_mode: Option<DisplayMode>,
file_path: Option<StandardizedPath>,
ctx: &mut ViewContext<Self>,
) -> Self {
#[cfg(not(target_family = "wasm"))]
let is_new_file = matches!(diff_type, Some(DiffType::Create { .. }));
ctx.subscribe_to_view(&editor, |me, _view, event, ctx| match event {
CodeEditorEvent::DiffUpdated => {
ctx.emit(InlineDiffViewEvent::DiffStatusUpdated);
}
CodeEditorEvent::ContentChanged { origin } => {
if origin.from_user() && !me.was_edited {
me.was_edited = true;
ctx.emit(InlineDiffViewEvent::UserEdited);
}
}
_ => {}
});
let model = Self {
editor,
diff_type,
file_path,
was_edited: false,
backing_file_id: None,
#[cfg(not(target_family = "wasm"))]
is_new_file,
};
model.apply_diffs_if_any(ctx);
if let Some(display_mode) = display_mode {
model.set_display_mode(display_mode, ctx);
}
model
}
/// Register a file with `FileModel` for save support.
///
/// The `session_type` determines whether the file is local or remote.
/// For `Local`, the file is registered by path on the local filesystem.
/// For `Remote`, the file is registered against the remote backend so
/// that `save()` / `delete()` dispatch over the wire via
/// `RemoteServerClient`.
///
/// This must be called after construction for non-WASM environments.
#[cfg(not(target_family = "wasm"))]
pub fn register_file(&mut self, session_type: &DiffSessionType, ctx: &mut ViewContext<Self>) {
let Some(file_path) = &self.file_path else {
return;
};
let file_model = FileModel::handle(ctx);
let file_id = match session_type {
DiffSessionType::Local => {
let Some(local_path) = file_path.to_local_path() else {
crate::safe_error!(
safe: (
"Failed to convert StandardizedPath to local path; diff will be \
read-only"
),
full: (
"Failed to convert StandardizedPath to local path: {file_path}; diff \
will be read-only"
)
);
return;
};
file_model.update(ctx, |file_model, ctx| {
file_model.register_file_path(&local_path, false, ctx)
})
}
DiffSessionType::Remote(host_id) => {
let host_id = host_id.clone();
let remote_path = file_path.clone();
file_model.update(ctx, |file_model, _ctx| {
file_model.register_remote_file(host_id, remote_path)
})
}
};
self.finish_file_registration(file_id, ctx);
}
/// Common registration logic: subscribes to events and sets the
/// backing file ID after a file has been registered with `FileModel`.
#[cfg(not(target_family = "wasm"))]
fn finish_file_registration(&mut self, file_id: FileId, ctx: &mut ViewContext<Self>) {
let file_model = FileModel::handle(ctx);
let version = self.editor.as_ref(ctx).version(ctx);
file_model.update(ctx, |file_model, _ctx| {
file_model.set_version(file_id, version);
});
self.backing_file_id = Some(file_id);
// Subscribe to FileModel events for this file.
ctx.subscribe_to_model(&file_model, move |_me, _file_model, event, ctx| {
if file_id == event.file_id() {
match event {
FileModelEvent::FileSaved { .. } => {
ctx.emit(InlineDiffViewEvent::FileSaved);
}
FileModelEvent::FailedToSave { error, .. } => {
ctx.emit(InlineDiffViewEvent::FailedToSave {
error: error.clone(),
});
}
_ => {}
}
}
});
ctx.emit(InlineDiffViewEvent::FileLoaded);
}
fn apply_diffs_if_any(&self, ctx: &mut ViewContext<Self>) {
let Some(diff) = self.diff_type.clone() else {
return;
};
let deltas = match diff {
DiffType::Create { delta } => vec![delta],
DiffType::Update { mut deltas, .. } => {
deltas.sort_by_key(|delta| delta.replacement_line_range.start);
deltas
}
DiffType::Delete { delta } => vec![delta],
};
if deltas.is_empty() {
return;
}
self.editor.update(ctx, |editor, ctx| {
editor.apply_diffs(deltas, ctx);
editor.toggle_diff_nav(None, ctx);
editor.set_pending_scroll(ScrollTrigger::new(
ScrollPosition::FocusedDiffHunk,
editor.buffer_version(ctx),
));
});
}
/// Saves the current editor content through `FileModel`, returning the
/// save's completion future. `FileSaved` / `FailedToSave` events still
/// fire alongside. `None` when no file is registered.
#[cfg(not(target_family = "wasm"))]
fn save_content(&self, ctx: &mut ViewContext<Self>) -> Option<SaveFuture> {
let file_id = self.backing_file_id?;
let content = self.editor.as_ref(ctx).text(ctx).into_string();
let version = self.editor.as_ref(ctx).version(ctx);
match FileModel::handle(ctx).update(ctx, |file_model, ctx| {
file_model.save(file_id, content, version, ctx)
}) {
Ok(save_future) => Some(save_future),
Err(err) => {
let error = Arc::new(err);
ctx.emit(InlineDiffViewEvent::FailedToSave {
error: error.clone(),
});
Some(futures::future::ready(Err(error)).boxed())
}
}
}
/// Accepts the diff by saving the editor content to the backing file,
/// returning the save's completion future. `None` when no file is
/// registered (WASM / restored conversations), in which case nothing is
/// dispatched.
pub fn accept_and_save_diff(&self, ctx: &mut ViewContext<Self>) -> Option<SaveFuture> {
#[cfg(not(target_family = "wasm"))]
{
self.save_content(ctx)
}
#[cfg(target_family = "wasm")]
{
let _ = ctx;
None
}
}
}
impl InlineDiffView {
pub fn file_path(&self) -> Option<&StandardizedPath> {
self.file_path.as_ref()
}
/// Base content of the diff (from the editor's diff model), if set.
pub fn base_content(&self, app: &AppContext) -> Option<String> {
self.editor
.as_ref(app)
.model
.as_ref(app)
.diff()
.as_ref(app)
.base()
.map(|base| base.to_string())
}
pub fn file_name(&self) -> Option<String> {
self.file_path()
.map(|p| p.file_name().unwrap_or_default().to_owned())
}
}
impl DiffViewer for InlineDiffView {
fn editor(&self) -> &ViewHandle<CodeEditorView> {
&self.editor
}
fn diff(&self) -> Option<&DiffType> {
self.diff_type.as_ref()
}
fn was_edited(&self) -> bool {
self.was_edited
}
fn set_display_mode(&self, mode: DisplayMode, ctx: &mut ViewContext<Self>) {
let is_delete = matches!(self.diff(), Some(DiffType::Delete { .. }));
let interaction_state = if self.backing_file_id.is_some() {
mode.interaction_state(is_delete)
} else {
// No file registered (e.g. WASM or restored conversations): always read-only.
InteractionState::Selectable
};
self.editor().update(ctx, |editor, ctx| {
editor.set_scroll_wheel_behavior(mode.scroll_wheel_behavior());
editor.set_vertical_expansion_behavior(mode.vertical_expansion_behavior(), ctx);
editor.set_vertical_scrollbar_appearance(mode.scrollbar_appearance());
editor.set_horizontal_scrollbar_appearance(mode.scrollbar_appearance());
editor.set_interaction_state(interaction_state, ctx);
editor.set_show_nav_bar(mode.show_nav_bar());
editor.set_nav_bar_behavior(NavBarBehavior::NotClosable, ctx);
});
}
fn restore_diff_base(&mut self, _ctx: &mut ViewContext<Self>) -> Result<(), String> {
// No-op when no file is registered (WASM / restored conversations).
if self.backing_file_id.is_none() {
return Ok(());
}
#[cfg(not(target_family = "wasm"))]
{
let file_id = self
.backing_file_id
.expect("backing_file_id must be Some — checked by early return above");
if self.is_new_file {
// For newly created files, delete instead of restoring. The
// delete's completion is observed via `FileSaved` events.
let version = self.editor.as_ref(_ctx).version(_ctx);
FileModel::handle(_ctx)
.update(_ctx, |file_model, ctx| {
file_model.delete(file_id, version, ctx)
})
.map(drop)
.map_err(|e| format!("Failed to delete file: {e:?}"))?;
return Ok(());
}
// For existing files, restore the base content from the editor's DiffModel.
let base_content = self
.editor
.as_ref(_ctx)
.model
.as_ref(_ctx)
.diff()
.as_ref(_ctx)
.base()
.ok_or_else(|| "Missing base content".to_string())?
.to_string();
let version = self.editor.as_ref(_ctx).version(_ctx);
// The revert save's completion is observed via `FileSaved` events.
FileModel::handle(_ctx)
.update(_ctx, |file_model, ctx| {
file_model.save(file_id, base_content, version, ctx)
})
.map(drop)
.map_err(|e| format!("Failed to save file: {e:?}"))?;
}
Ok(())
}
}
impl Entity for InlineDiffView {
type Event = InlineDiffViewEvent;
}
impl View for InlineDiffView {
fn ui_name() -> &'static str {
"InlineDiffView"
}
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
ChildView::new(&self.editor).finish()
}
}
impl TypedActionView for InlineDiffView {
type Action = ();
}