Skip to content
Open
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
9 changes: 9 additions & 0 deletions app/src/ai/blocklist/action_model/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,15 @@ async fn read_binary_file_context(
Ok(content) => content,
Err(FileLoadError::DoesNotExist) => return Ok(BinaryFileReadResult::NotFound),
Err(FileLoadError::IOError(e)) => return Err(anyhow::anyhow!(e)),
Err(FileLoadError::TooLarge {
size_estimate,
limit_bytes,
}) => {
return Ok(BinaryFileReadResult::TooLarge {
size_bytes: size_estimate.unwrap_or(limit_bytes.saturating_add(1)) as usize,
limit_bytes: limit_bytes as usize,
});
}
};

let mime_type = from_path(path).first_or_octet_stream().to_string();
Expand Down
29 changes: 28 additions & 1 deletion app/src/code/global_buffer_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use super::buffer_location::{LocalOrRemotePath, SyncClock};
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
use lsp::LspManagerModelEvent;
use warp_files::{FileModelEvent, FileModel};
use warp_files::{FileModelEvent, FileModel, MAX_LOADABLE_FILE_SIZE_BYTES};
use warp_editor::content::text::IndentBehavior;
use warp_editor::content::text::IndentUnit;
use warp_editor::content::buffer::EditOrigin;
Expand Down Expand Up @@ -493,6 +493,19 @@ impl GlobalBufferModel {
let Some(state) = self.buffers.get_mut(&file_id) else {
return;
};
#[cfg(feature = "local_fs")]
{
if content.len() as u64 > MAX_LOADABLE_FILE_SIZE_BYTES {
ctx.emit(GlobalBufferModelEvent::FailedToLoad {
file_id,
error: Rc::new(FileLoadError::TooLarge {
size_estimate: Some(content.len() as u64),
limit_bytes: MAX_LOADABLE_FILE_SIZE_BYTES,
}),
});
return;
}
}

let Some(buffer) = state.buffer.upgrade(ctx) else {
self.cleanup_file_id(file_id, ctx);
Expand Down Expand Up @@ -709,6 +722,20 @@ impl GlobalBufferModel {
base_version,
new_version,
} => {
if self.buffers.get(id).is_some_and(|state| !state.is_loaded()) {
if let Some(state) = self.buffers.get_mut(id) {
state.set_initial_content_version(*new_version);
}
self.populate_buffer_with_read_content(
*id,
content,
*base_version,
*new_version,
true,
ctx,
);
return;
}
if let Some(buffer) = self.buffer_handle_for_id(*id, ctx) {
if buffer.as_ref(ctx).version_match(base_version) {
self.populate_buffer_with_read_content(
Expand Down
123 changes: 121 additions & 2 deletions app/src/code/global_buffer_model_tests.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
use std::cell::RefCell;
use std::rc::Rc;

use lsp::LspManagerModel;
use remote_server::proto::TextEdit;
use repo_metadata::RepoMetadataModel;
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::watcher::DirectoryWatcher;
use warp_files::FileModel;
use warp_editor::content::buffer::Buffer;
use warp_files::{FileModel, FileModelEvent, MAX_LOADABLE_FILE_SIZE_BYTES};
use warp_util::content_version::ContentVersion;
use warp_util::file::{FileId, FileLoadError};
use warp_util::host_id::HostId;
use warp_util::standardized_path::StandardizedPath;
use warpui::{App, ModelHandle, SingletonEntity};

use super::{BufferSource, CharOffsetEdit, GlobalBufferModel, PendingEditBatch};
use super::{
BufferSource, CharOffsetEdit, GlobalBufferModel, GlobalBufferModelEvent, InternalBufferState,
PendingEditBatch,
};
use crate::test_util::settings::initialize_settings_for_tests;

// ── Test-only helpers on GlobalBufferModel ────────────────────────
Expand Down Expand Up @@ -122,6 +130,117 @@ fn test_path() -> StandardizedPath {
StandardizedPath::try_new("/test/file.txt").unwrap()
}

fn seed_local_buffer(app: &mut App, content: &str, loaded: bool) -> (FileId, ModelHandle<Buffer>) {
let buffer = app.add_model(|_| Buffer::default());
let version = ContentVersion::new();
if !content.is_empty() {
buffer.update(app, |buffer, ctx| {
buffer.replace_all(content, ctx);
buffer.set_version(version);
});
}

let file_id = FileId::new();
gbm(app).update(app, |model, _| {
model.buffers.insert(
file_id,
InternalBufferState {
buffer: buffer.downgrade(),
latest_buffer_version: None,
pending_diff_parse: None,
source: BufferSource::Local {
base_content_version: loaded.then_some(version),
initial_content_version: loaded.then_some(version),
},
},
);
});
(file_id, buffer)
}

#[test]
fn oversized_content_is_rejected_before_initial_and_fallback_population() {
App::test((), |mut app| async move {
init_app(&mut app);
app.add_singleton_model(GlobalBufferModel::new);
let (initial_id, initial_buffer) = seed_local_buffer(&mut app, "", false);
let (fallback_id, fallback_buffer) = seed_local_buffer(&mut app, "preserved", true);
let oversized = "x".repeat(MAX_LOADABLE_FILE_SIZE_BYTES as usize + 1);
let version = ContentVersion::new();
let failed_file_ids = Rc::new(RefCell::new(Vec::new()));
let global_buffer = gbm(&app);
app.update(|ctx| {
let failed_file_ids = failed_file_ids.clone();
ctx.subscribe_to_model(&global_buffer, move |_, event, _| {
if let GlobalBufferModelEvent::FailedToLoad { file_id, error } = event {
assert!(matches!(error.as_ref(), FileLoadError::TooLarge { .. }));
failed_file_ids.borrow_mut().push(*file_id);
}
});
});

gbm(&app).update(&mut app, |model, ctx| {
model.populate_buffer_with_read_content(
initial_id, &oversized, version, version, true, ctx,
);
model.populate_buffer_with_read_content(
fallback_id,
&oversized,
version,
version,
false,
ctx,
);
});

app.read(|ctx| {
assert_eq!(initial_buffer.as_ref(ctx).text().into_string(), "");
assert_eq!(
fallback_buffer.as_ref(ctx).text().into_string(),
"preserved"
);
});
let model = gbm(&app);
app.read(|ctx| {
assert!(!model.as_ref(ctx).buffer_loaded(initial_id));
assert!(model.as_ref(ctx).buffer_loaded(fallback_id));
});
assert_eq!(
failed_file_ids.borrow().as_slice(),
&[initial_id, fallback_id]
);
})
}

#[test]
fn first_file_update_after_load_failure_populates_buffer() {
App::test((), |mut app| async move {
init_app(&mut app);
app.add_singleton_model(GlobalBufferModel::new);
let (file_id, buffer) = seed_local_buffer(&mut app, "", false);
let base_version = ContentVersion::new();
let new_version = ContentVersion::new();
let event = FileModelEvent::FileUpdated {
id: file_id,
content: "now loadable".to_string(),
base_version,
new_version,
};
let files = FileModel::handle(&app);

gbm(&app).update(&mut app, |model, ctx| {
model.handle_file_model_events(files, &event, ctx);
});
let global_buffer = gbm(&app);

app.read(|ctx| {
assert_eq!(buffer.as_ref(ctx).text().into_string(), "now loadable");
assert_eq!(buffer.as_ref(ctx).version(), new_version);
assert!(global_buffer.as_ref(ctx).buffer_loaded(file_id));
});
})
}

// ── Pending edit batch: discard on server push ───────────────────

#[test]
Expand Down
42 changes: 41 additions & 1 deletion app/src/code/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::ops::AddAssign;

use pathfinder_geometry::rect::RectF;
use warp_errors::{ErrorExt, register_error};
use warp_util::file::FileSaveError;
use warp_util::file::{FileLoadError, FileSaveError};
use warpui::AppContext;
use warpui::elements::DropTargetData;

Expand Down Expand Up @@ -53,6 +53,42 @@ impl ErrorExt for ImmediateSaveError {
}
register_error!(ImmediateSaveError);

pub(crate) fn file_load_error_message(error: &FileLoadError) -> String {
match error {
FileLoadError::TooLarge {
size_estimate,
limit_bytes,
} => {
let limit = format_file_size(*limit_bytes);
match size_estimate {
Some(size) => format!(
"File is larger than the {limit} limit (reported size ~{}).",
format_file_size(*size)
),
None => format!("File is larger than the {limit} limit."),
}
}
FileLoadError::DoesNotExist | FileLoadError::IOError(_) => {
"Failed to load file.".to_string()
}
}
}

fn format_file_size(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{bytes} {}", UNITS[unit_index])
} else {
format!("{size:.1} {}", UNITS[unit_index])
}
}

/// Trait to determine whether we should show the comment editor based on state held
/// by the parent of the [`CodeEditorView`].
pub trait ShowCommentEditorProvider: Debug + 'static {
Expand Down Expand Up @@ -171,3 +207,7 @@ impl DropTargetData for EditorTabBarDropTargetData {
self
}
}

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;
29 changes: 29 additions & 0 deletions app/src/code/mod_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::io;

use super::{FileLoadError, file_load_error_message};

#[test]
fn file_load_error_message_describes_oversized_files() {
assert_eq!(
file_load_error_message(&FileLoadError::TooLarge {
size_estimate: Some(2 * 1024 * 1024 * 1024),
limit_bytes: 100 * 1024 * 1024,
}),
"File is larger than the 100.0 MiB limit (reported size ~2.0 GiB)."
);
assert_eq!(
file_load_error_message(&FileLoadError::TooLarge {
size_estimate: None,
limit_bytes: 100 * 1024 * 1024,
}),
"File is larger than the 100.0 MiB limit."
);
}

#[test]
fn file_load_error_message_keeps_generic_io_failure_copy() {
assert_eq!(
file_load_error_message(&FileLoadError::IOError(io::Error::other("failure"))),
"Failed to load file."
);
}
16 changes: 12 additions & 4 deletions app/src/code/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use warp_core::features::FeatureFlag;
use warp_core::ui::appearance::Appearance;
use warp_core::ui::icons::ICON_DIMENSIONS;
use warp_editor::render::element::VerticalExpansionBehavior;
use warp_util::file::FileLoadError;
use warp_util::path::LineAndColumnArg;
#[cfg(feature = "local_fs")]
use warpui::clipboard::ClipboardContent;
Expand Down Expand Up @@ -41,7 +42,10 @@ use crate::code::editor::view::CodeEditorRenderOptions;
use crate::code::editor_management::CodeEditorStatus;
use crate::code::global_buffer_model::GlobalBufferModel;
use crate::code::local_code_editor::ShowFindReferencesCard;
use crate::code::{EditorTabBarDropTargetData, ImmediateSaveError, SaveOutcome, SaveStatus};
use crate::code::{
EditorTabBarDropTargetData, ImmediateSaveError, SaveOutcome, SaveStatus,
file_load_error_message,
};
use crate::editor::InteractionState;
use crate::input::Vector2F;
use crate::menu::{MenuItem, MenuItemFields};
Expand Down Expand Up @@ -516,7 +520,7 @@ impl CodeView {
return;
}
log::warn!("Failed to load file. {err:?}");
CodeView::display_load_failure(ctx.window_id(), ctx);
CodeView::display_load_failure(ctx.window_id(), err, ctx);
}
LocalCodeEditorEvent::SelectionAddedAsContext {
relative_file_path,
Expand Down Expand Up @@ -943,9 +947,13 @@ impl CodeView {
}
}

fn display_load_failure(window_id: WindowId, ctx: &mut ViewContext<Self>) {
fn display_load_failure(
window_id: WindowId,
error: &FileLoadError,
ctx: &mut ViewContext<Self>,
) {
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
let toast = DismissibleToast::error(String::from("Failed to load file."))
let toast = DismissibleToast::error(file_load_error_message(error))
.with_object_id("failed_to_load_file".to_string());
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
});
Expand Down
Loading
Loading