use crate::{
buffer::{BufferFile, BufferWriteError},
ecs::{
components::{
buffer::{
BufferEntity, BufferRevision, BufferText, EditorBuffer, EditorView,
FocusedEditorView, ViewEntity,
},
cursor::{ByteIndex, CursorPosition},
layout::{TextSpanLayout, ViewportState, VisibleTextRange},
vim::VimModalState,
},
events::{
command::CommandRequested,
cursor::{CursorMoveRequested, CursorMoved},
edit::{BufferEditRequested, BufferEdited},
lifecycle::LifecycleRequested,
status::{StatusMessage, StatusMessageRequested},
},
schedules::{EditorSet, EditorStartupSet},
},
fs_utils::FilesystemConfig,
text_stream::TextByteStream,
vim::{QuitPolicy, VimCommand, VimCursor, VimError, WriteQuitPolicy},
};
use bevy::{
app::AppExit,
prelude::{
App, Commands, Entity, IntoScheduleConfigs, MessageReader, MessageWriter, Plugin, Query,
Res, Resource, Startup, Update, With,
},
};
#[derive(Clone, Debug, Resource)]
pub struct InitialEditorBuffer {
pub stream: TextByteStream,
pub file: BufferFile,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BufferPlugin;
impl Plugin for BufferPlugin {
fn build(&self, app: &mut App) {
let _app = app
.init_resource::<FilesystemConfig>()
.add_message::<AppExit>()
.add_systems(
Startup,
spawn_editor_buffer.in_set(EditorStartupSet::Buffer),
)
.add_systems(
Update,
(
apply_buffer_edit_requests,
apply_command_requests,
apply_cursor_move_requests,
apply_status_message_requests,
apply_lifecycle_requests,
)
.chain()
.in_set(EditorSet::Edit),
);
}
}
fn spawn_editor_buffer(mut commands: Commands, initial_buffer: Res<InitialEditorBuffer>) {
let initial_buffer = initial_buffer.into_inner();
let text_stream = &initial_buffer.stream;
let buffer = commands
.spawn((
EditorBuffer,
initial_buffer.file.clone(),
BufferText {
stream: text_stream.clone(),
},
BufferRevision {
revision: text_stream.revision(),
},
))
.id();
let _view = commands.spawn((
EditorView {
buffer: BufferEntity(buffer),
},
FocusedEditorView,
CursorPosition {
byte_index: ByteIndex(0),
desired_column: None,
},
ViewportState {
viewport: crate::domain::viewport::TextViewport::default(),
pending_intent: None,
},
VisibleTextRange::default(),
TextSpanLayout::default(),
VimModalState::default(),
));
}
fn apply_command_requests(
mut requests: MessageReader<CommandRequested>,
mut status_requests: MessageWriter<StatusMessageRequested>,
mut lifecycle_requests: MessageWriter<LifecycleRequested>,
filesystem_config: Res<FilesystemConfig>,
mut query: Query<(Entity, &BufferText, &mut BufferFile), With<EditorBuffer>>,
) {
let filesystem_config = filesystem_config.into_inner();
for request in requests.read() {
for (entity, buffer_text, mut buffer_file) in &mut query {
if BufferEntity(entity) != request.target {
continue;
}
match execute_buffer_command(
request.command,
&buffer_text.stream,
&mut buffer_file,
filesystem_config,
) {
Ok(outcome) => {
apply_command_outcome(
request.source,
&outcome,
&mut status_requests,
&mut lifecycle_requests,
);
}
Err(error) => {
let _sent = status_requests.write(StatusMessageRequested {
target: request.source,
message: StatusMessage::Error(error),
});
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct CommandOutcome {
should_quit: bool,
status_text: Option<String>,
}
impl CommandOutcome {
const fn continue_with(status_text: Option<String>) -> Self {
Self {
should_quit: false,
status_text,
}
}
const fn quit(status_text: Option<String>) -> Self {
Self {
should_quit: true,
status_text,
}
}
const fn should_quit(&self) -> bool {
self.should_quit
}
fn status_text(&self) -> Option<&str> {
self.status_text.as_deref()
}
}
fn execute_buffer_command(
command: VimCommand,
stream: &TextByteStream,
buffer: &mut BufferFile,
config: &FilesystemConfig,
) -> Result<CommandOutcome, VimError> {
match command {
VimCommand::Write => {
write_buffer(stream, buffer, config).map(CommandOutcome::continue_with)
}
VimCommand::Quit(QuitPolicy::PreserveChanges) if buffer.is_modified(stream) => {
Err(VimError::NoWriteSinceLastChange)
}
VimCommand::Quit(QuitPolicy::PreserveChanges | QuitPolicy::Force) => {
Ok(CommandOutcome::quit(None))
}
VimCommand::WriteQuit(WriteQuitPolicy::AlwaysWrite) => {
write_buffer(stream, buffer, config).map(CommandOutcome::quit)
}
VimCommand::WriteQuit(WriteQuitPolicy::WriteIfModified) => {
if buffer.is_modified(stream) {
write_buffer(stream, buffer, config).map(CommandOutcome::quit)
} else {
Ok(CommandOutcome::quit(None))
}
}
}
}
fn write_buffer(
stream: &TextByteStream,
buffer: &mut BufferFile,
config: &FilesystemConfig,
) -> Result<Option<String>, VimError> {
buffer
.write(stream, config)
.map(|report| Some(report.status_text()))
.map_err(|error| match error {
BufferWriteError::NoFileName => VimError::NoFileName,
BufferWriteError::File(_source) => VimError::CantOpenFileForWriting,
BufferWriteError::Io(_source) => VimError::CantOpenFileForWriting,
})
}
fn apply_command_outcome(
target: ViewEntity,
outcome: &CommandOutcome,
status_requests: &mut MessageWriter<StatusMessageRequested>,
lifecycle_requests: &mut MessageWriter<LifecycleRequested>,
) {
if let Some(status_text) = outcome.status_text() {
let _sent = status_requests.write(StatusMessageRequested {
target,
message: StatusMessage::Info(status_text.to_owned()),
});
} else {
let _sent = status_requests.write(StatusMessageRequested {
target,
message: StatusMessage::Clear,
});
}
if outcome.should_quit() {
let _sent = lifecycle_requests.write(LifecycleRequested::Quit);
}
}
fn apply_buffer_edit_requests(
mut requests: MessageReader<BufferEditRequested>,
mut edited: MessageWriter<BufferEdited>,
mut query: Query<(Entity, &mut BufferText, &mut BufferRevision), With<EditorBuffer>>,
) {
for request in requests.read() {
for (entity, mut buffer_text, mut revision) in &mut query {
if BufferEntity(entity) != request.target {
continue;
}
let mut cursor = VimCursor::new();
cursor.set_byte_index(buffer_text.stream.as_str(), 0);
let Ok(report) = request
.edit
.clone()
.apply_with_cursor(&mut buffer_text.stream, &mut cursor)
else {
continue;
};
revision.revision = buffer_text.stream.revision();
let _sent = edited.write(BufferEdited {
target: BufferEntity(entity),
previous_revision: BufferRevision {
revision: report.previous_revision,
},
current_revision: BufferRevision {
revision: report.current_revision,
},
});
}
}
}
fn apply_cursor_move_requests(
mut requests: MessageReader<CursorMoveRequested>,
mut moved: MessageWriter<CursorMoved>,
buffers: Query<&BufferText, With<EditorBuffer>>,
mut views: Query<(Entity, &EditorView, &mut CursorPosition)>,
) {
for request in requests.read() {
for (entity, view, mut cursor_position) in &mut views {
if ViewEntity(entity) != request.target {
continue;
}
let Ok(buffer_text) = buffers.get(view.buffer.get()) else {
continue;
};
let previous = cursor_position.byte_index;
let mut cursor = VimCursor::new();
cursor.set_byte_index(
buffer_text.stream.as_str(),
cursor_position.byte_index.get(),
);
cursor.set_desired_column(cursor_position.desired_column);
cursor.set_byte_index(buffer_text.stream.as_str(), request.byte_index.get());
cursor.set_desired_column(request.desired_column);
cursor_position.byte_index = ByteIndex(cursor.byte_index());
cursor_position.desired_column = cursor.desired_column();
if previous != cursor_position.byte_index {
let _sent = moved.write(CursorMoved {
target: ViewEntity(entity),
previous_byte_index: previous,
current_byte_index: cursor_position.byte_index,
desired_column: cursor_position.desired_column,
});
}
}
}
}
fn apply_status_message_requests(
mut requests: MessageReader<StatusMessageRequested>,
mut query: Query<(Entity, &mut VimModalState), With<EditorView>>,
) {
for request in requests.read() {
for (entity, mut modal_state) in &mut query {
if ViewEntity(entity) != request.target {
continue;
}
match &request.message {
StatusMessage::Clear => modal_state.status.clear(),
StatusMessage::Info(message) => modal_state.status.set_info(message.clone()),
StatusMessage::Error(error) => modal_state.status.set_error(*error),
}
}
}
}
fn apply_lifecycle_requests(
mut requests: MessageReader<LifecycleRequested>,
mut app_exit: MessageWriter<AppExit>,
) {
for request in requests.read() {
match request {
LifecycleRequested::Quit => {
let _sent = app_exit.write(AppExit::Success);
}
}
}
}
#[cfg(test)]
mod tests {
use super::{BufferPlugin, InitialEditorBuffer};
use crate::{
buffer::{BufferEdit, BufferFile},
domain::viewport::TextViewport,
ecs::{
components::{
buffer::{
BufferEntity, BufferRevision, BufferText, EditorBuffer, EditorView, ViewEntity,
},
cursor::{ByteIndex, CursorPosition},
layout::{TextSpanLayout, ViewportState, VisibleTextRange},
vim::VimModalState,
},
events::{
command::CommandRequested,
cursor::CursorMoveRequested,
edit::{BufferEditRequested, BufferEdited},
status::{StatusMessage, StatusMessageRequested},
},
},
fs_utils::FilesystemConfig,
text_stream::TextByteStream,
vim::{QuitPolicy, VimCommand, VimError, VimStatusMessage},
};
use bevy::{
app::AppExit,
prelude::{App, Entity, IntoScheduleConfigs, MessageReader, Resource, Update, With},
};
use proptest::prelude::*;
use std::{
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[derive(Default, Resource)]
struct CapturedEdits {
events: Vec<BufferEdited>,
}
#[derive(Default, Resource)]
struct CapturedExits {
events: Vec<AppExit>,
}
fn capture_edits(
mut reader: MessageReader<BufferEdited>,
mut captured: bevy::prelude::ResMut<CapturedEdits>,
) {
captured.events.extend(reader.read().copied());
}
fn capture_exits(
mut reader: MessageReader<AppExit>,
mut captured: bevy::prelude::ResMut<CapturedExits>,
) {
captured.events.extend(reader.read().cloned());
}
fn temp_file_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("alma-{name}-{}-{nanos}.txt", std::process::id()))
}
fn temp_filesystem_config() -> FilesystemConfig {
FilesystemConfig::from_workspace_root(std::env::temp_dir())
.expect("temporary directory should be a workspace root")
}
fn editor_buffer_entity(app: &mut App) -> Entity {
let mut query = app
.world_mut()
.query_filtered::<Entity, With<crate::ecs::components::buffer::EditorBuffer>>();
query
.iter(app.world())
.next()
.expect("editor buffer should exist")
}
fn editor_view_entity(app: &mut App) -> Entity {
let mut query = app.world_mut().query_filtered::<Entity, With<EditorView>>();
query
.iter(app.world())
.next()
.expect("editor view should exist")
}
fn view_for_buffer(app: &mut App, buffer: Entity) -> Entity {
let mut query = app.world_mut().query::<(Entity, &EditorView)>();
query
.iter(app.world())
.find_map(|(entity, view)| (view.buffer == BufferEntity(buffer)).then_some(entity))
.expect("editor view should exist for buffer")
}
fn spawn_editor_buffer_entity(app: &mut App, text: &str) -> Entity {
let stream = TextByteStream::new(text);
let revision = stream.revision();
let buffer = app
.world_mut()
.spawn((
EditorBuffer,
BufferFile::scratch(revision),
BufferText { stream },
BufferRevision { revision },
))
.id();
let _view = app
.world_mut()
.spawn((
EditorView {
buffer: BufferEntity(buffer),
},
CursorPosition {
byte_index: ByteIndex(0),
desired_column: None,
},
ViewportState {
viewport: TextViewport::default(),
pending_intent: None,
},
VisibleTextRange::default(),
TextSpanLayout::default(),
VimModalState::default(),
))
.id();
buffer
}
fn vim_modal_state(app: &bevy::prelude::App, entity: Entity) -> &VimModalState {
app.world()
.get::<VimModalState>(entity)
.expect("editor buffer should have Vim modal state")
}
#[test]
fn buffer_plugin_applies_valid_edits_and_rejects_invalid_ranges() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("aλ"),
file: BufferFile::scratch(0),
})
.init_resource::<CapturedEdits>()
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin)
.add_systems(Update, capture_edits);
app.update();
let target = editor_buffer_entity(&mut app);
let source = editor_view_entity(&mut app);
let _message = app.world_mut().write_message(BufferEditRequested {
target: BufferEntity(target),
edit: BufferEdit::Delete { range: 1..2 },
});
app.update();
let buffer = app
.world()
.get::<BufferText>(target)
.expect("editor buffer should exist");
let cursor = app
.world()
.get::<CursorPosition>(source)
.expect("editor view should exist");
assert_eq!(buffer.stream.as_str(), "aλ");
assert_eq!(cursor.byte_index, ByteIndex(0));
assert!(app.world().resource::<CapturedEdits>().events.is_empty());
let _message = app.world_mut().write_message(BufferEditRequested {
target: BufferEntity(target),
edit: BufferEdit::Insert {
byte_index: "aλ".len(),
text: String::from("!"),
},
});
app.update();
app.update();
let mut query = app.world_mut().query::<&BufferText>();
let buffer = query
.iter(app.world())
.next()
.expect("editor buffer should exist");
assert_eq!(buffer.stream.as_str(), "aλ!");
assert_eq!(
app.world().resource::<CapturedEdits>().events,
vec![BufferEdited {
target: BufferEntity(target),
previous_revision: BufferRevision { revision: 0 },
current_revision: BufferRevision { revision: 1 },
}]
);
}
#[test]
fn command_request_write_updates_saved_revision_after_successful_write() {
let path = temp_file_path("write-success");
let mut app = App::new();
let _app = app
.insert_resource(temp_filesystem_config())
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("saved text"),
file: BufferFile::backed_by(path.clone(), 0),
})
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin);
app.update();
let target = editor_buffer_entity(&mut app);
let source = editor_view_entity(&mut app);
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(target),
source: ViewEntity(source),
command: VimCommand::Write,
});
app.update();
app.update();
let mut query = app.world_mut().query::<&BufferText>();
let stream = query
.iter(app.world())
.next()
.expect("editor buffer should exist")
.stream
.clone();
let buffer_file = app
.world()
.get::<BufferFile>(target)
.expect("target buffer file should exist");
assert!(!buffer_file.is_modified(&stream));
assert_eq!(
fs::read_to_string(&path).expect("write should create file contents"),
"saved text"
);
assert_eq!(
vim_modal_state(&app, source).status.message(),
Some(&VimStatusMessage::Info(format!(
"\"{}\" 1L, 10B written",
path.canonicalize()
.expect("written path should canonicalize")
.display()
)))
);
let _cleanup = fs::remove_file(path);
}
#[test]
fn command_requests_use_the_target_buffer_file() {
let first_path = temp_file_path("targeted-write-first");
let second_path = temp_file_path("targeted-write-second");
let mut app = App::new();
let _app = app
.insert_resource(temp_filesystem_config())
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("first"),
file: BufferFile::backed_by(first_path.clone(), 0),
})
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin);
app.update();
let first = editor_buffer_entity(&mut app);
let first_view = editor_view_entity(&mut app);
let second = spawn_editor_buffer_entity(&mut app, "second");
let second_view = view_for_buffer(&mut app, second);
app.world_mut()
.get_mut::<BufferFile>(second)
.expect("second buffer file should exist")
.clone_from(&BufferFile::backed_by(second_path.clone(), 0));
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(second),
source: ViewEntity(second_view),
command: VimCommand::Write,
});
app.update();
app.update();
assert!(!first_path.exists());
assert_eq!(
fs::read_to_string(&second_path).expect("write should create file contents"),
"second"
);
let first_file = app
.world()
.get::<BufferFile>(first)
.expect("first buffer file should exist");
let first_stream = &app
.world()
.get::<BufferText>(first)
.expect("first buffer text should exist")
.stream;
let second_file = app
.world()
.get::<BufferFile>(second)
.expect("second buffer file should exist");
let second_stream = &app
.world()
.get::<BufferText>(second)
.expect("second buffer text should exist")
.stream;
assert!(!first_file.is_modified(first_stream));
assert!(!second_file.is_modified(second_stream));
assert_eq!(vim_modal_state(&app, first_view).status.message(), None);
assert_eq!(
vim_modal_state(&app, second_view).status.message(),
Some(&VimStatusMessage::Info(format!(
"\"{}\" 1L, 6B written",
second_path
.canonicalize()
.expect("written path should canonicalize")
.display()
)))
);
let _cleanup = fs::remove_file(second_path);
}
#[test]
fn command_request_failed_write_leaves_saved_revision_unchanged() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("scratch"),
file: BufferFile::scratch(99),
})
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin);
app.update();
let target = editor_buffer_entity(&mut app);
let source = editor_view_entity(&mut app);
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(target),
source: ViewEntity(source),
command: VimCommand::Write,
});
app.update();
app.update();
let mut query = app.world_mut().query::<&BufferText>();
let stream = query
.iter(app.world())
.next()
.expect("editor buffer should exist")
.stream
.clone();
let buffer_file = app
.world()
.get::<BufferFile>(target)
.expect("target buffer file should exist");
assert!(buffer_file.is_modified(&stream));
assert_eq!(
vim_modal_state(&app, source).status.message(),
Some(&VimStatusMessage::Error(VimError::NoFileName))
);
}
#[test]
fn command_request_quit_refuses_modified_buffer_but_force_quits() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("dirty"),
file: BufferFile::scratch(0),
})
.init_resource::<CapturedExits>()
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin)
.add_systems(
Update,
capture_exits.after(crate::ecs::schedules::EditorSet::Edit),
);
app.update();
let target = editor_buffer_entity(&mut app);
let source = editor_view_entity(&mut app);
let _message = app.world_mut().write_message(BufferEditRequested {
target: BufferEntity(target),
edit: BufferEdit::Insert {
byte_index: "dirty".len(),
text: String::from("!"),
},
});
app.update();
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(target),
source: ViewEntity(source),
command: VimCommand::Quit(QuitPolicy::PreserveChanges),
});
app.update();
app.update();
assert_eq!(
vim_modal_state(&app, source).status.message(),
Some(&VimStatusMessage::Error(VimError::NoWriteSinceLastChange))
);
assert!(app.world().resource::<CapturedExits>().events.is_empty());
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(target),
source: ViewEntity(source),
command: VimCommand::Quit(QuitPolicy::Force),
});
app.update();
assert_eq!(
app.world().resource::<CapturedExits>().events,
vec![AppExit::Success]
);
}
#[test]
fn command_request_quit_exits_cleanly_when_buffer_is_unmodified() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("clean"),
file: BufferFile::scratch(0),
})
.init_resource::<CapturedExits>()
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin)
.add_systems(
Update,
capture_exits.after(crate::ecs::schedules::EditorSet::Edit),
);
app.update();
let target = editor_buffer_entity(&mut app);
let source = editor_view_entity(&mut app);
let _message = app.world_mut().write_message(CommandRequested {
target: BufferEntity(target),
source: ViewEntity(source),
command: VimCommand::Quit(QuitPolicy::PreserveChanges),
});
app.update();
assert_eq!(
app.world().resource::<CapturedExits>().events,
vec![AppExit::Success]
);
}
#[test]
fn buffer_and_cursor_owner_requests_only_mutate_target_entity() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("first"),
file: BufferFile::scratch(0),
})
.init_resource::<CapturedEdits>()
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin)
.add_systems(Update, capture_edits);
app.update();
let first = editor_buffer_entity(&mut app);
let first_view = editor_view_entity(&mut app);
let second = spawn_editor_buffer_entity(&mut app, "second");
let second_view = view_for_buffer(&mut app, second);
let _message = app.world_mut().write_message(BufferEditRequested {
target: BufferEntity(second),
edit: BufferEdit::Insert {
byte_index: "second".len(),
text: String::from("!"),
},
});
let _message = app.world_mut().write_message(CursorMoveRequested {
target: ViewEntity(second_view),
byte_index: ByteIndex("sec".len()),
desired_column: None,
});
app.update();
app.update();
let first_buffer = app
.world()
.get::<BufferText>(first)
.expect("first buffer should exist");
let first_cursor = app
.world()
.get::<CursorPosition>(first_view)
.expect("first cursor should exist");
let second_buffer = app
.world()
.get::<BufferText>(second)
.expect("second buffer should exist");
let second_cursor = app
.world()
.get::<CursorPosition>(second_view)
.expect("second cursor should exist");
assert_eq!(first_buffer.stream.as_str(), "first");
assert_eq!(first_cursor.byte_index, ByteIndex(0));
assert_eq!(second_buffer.stream.as_str(), "second!");
assert_eq!(second_cursor.byte_index, ByteIndex("sec".len()));
assert_eq!(
app.world().resource::<CapturedEdits>().events,
vec![BufferEdited {
target: BufferEntity(second),
previous_revision: BufferRevision { revision: 0 },
current_revision: BufferRevision { revision: 1 },
}]
);
}
#[test]
fn status_requests_only_mutate_target_entity() {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("first"),
file: BufferFile::scratch(0),
})
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin);
app.update();
let _first = editor_buffer_entity(&mut app);
let first_view = editor_view_entity(&mut app);
let second = spawn_editor_buffer_entity(&mut app, "second");
let second_view = view_for_buffer(&mut app, second);
let _message = app.world_mut().write_message(StatusMessageRequested {
target: ViewEntity(second_view),
message: StatusMessage::Error(VimError::NoFileName),
});
app.update();
assert_eq!(vim_modal_state(&app, first_view).status.message(), None);
assert_eq!(
vim_modal_state(&app, second_view).status.message(),
Some(&VimStatusMessage::Error(VimError::NoFileName))
);
}
proptest! {
#[test]
fn targeted_buffer_edits_only_mutate_selected_entity(
inserted in "\\PC{0,16}",
target_second in any::<bool>(),
) {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("first"),
file: BufferFile::scratch(0),
})
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(BufferPlugin);
app.update();
let first = editor_buffer_entity(&mut app);
let first_view = editor_view_entity(&mut app);
let second = spawn_editor_buffer_entity(&mut app, "second");
let second_view = view_for_buffer(&mut app, second);
app.world_mut()
.get_mut::<VimModalState>(first_view)
.expect("first modal state should exist")
.status
.set_info("first modal");
app.world_mut()
.get_mut::<VimModalState>(second_view)
.expect("second modal state should exist")
.status
.set_error(VimError::NoFileName);
let target = if target_second { second } else { first };
let byte_index = if target_second {
"second".len()
} else {
"first".len()
};
let _message = app.world_mut().write_message(BufferEditRequested {
target: BufferEntity(target),
edit: BufferEdit::Insert {
byte_index,
text: inserted.clone(),
},
});
app.update();
let first_text = app
.world()
.get::<BufferText>(first)
.expect("first buffer should exist")
.stream
.as_str()
.to_owned();
let second_text = app
.world()
.get::<BufferText>(second)
.expect("second buffer should exist")
.stream
.as_str()
.to_owned();
if target_second {
prop_assert_eq!(first_text, "first");
prop_assert_eq!(second_text, format!("second{inserted}"));
} else {
prop_assert_eq!(first_text, format!("first{inserted}"));
prop_assert_eq!(second_text, "second");
}
prop_assert_eq!(
vim_modal_state(&app, first_view).status.message(),
Some(&VimStatusMessage::Info(String::from("first modal")))
);
prop_assert_eq!(
vim_modal_state(&app, second_view).status.message(),
Some(&VimStatusMessage::Error(VimError::NoFileName))
);
}
}
}