use {
reovim_driver_clipboard::{ClipboardKey, ClipboardProviderRegistry},
reovim_driver_command_types::{CommandContext, CommandResult, RuntimeSignal},
reovim_driver_layout::{
LayerId, NavigateDirection, OverlayConstraints, Rect, SplitDirection, WindowPlacement,
},
reovim_driver_undo::{UndoKey, UndoProviderRegistry},
reovim_kernel::api::v1::{
BufferId, CommandId, Edit, KernelContext, ModeId, OptionValue, Position, TabId, UndoResult,
WindowId,
events::kernel::{
CursorMoved, LayoutChangeKind, LayoutChanged, SplitDirection as KernelSplitDirection,
},
},
};
use crate::{
Selection, Session, SessionExtension, Window,
api::{
BufferApi, BufferError, ChangeTracker, ClipboardApi, CommandApi, CommandExecutor,
CompositorApi, CompositorError, ExtensionApi, ModeApi, ModeError, RegisterApi,
RegisterContent, StateChanges, UndoApi, WindowApi, WindowError,
},
transition::{PopResult, TransitionContext},
};
pub struct SessionRuntime<'a> {
owner: Option<crate::ClientId>,
session: &'a mut Session,
mode_stack: &'a mut reovim_kernel::api::v1::ModeStack,
windows: &'a mut crate::WindowLayout,
extensions: &'a mut crate::ExtensionMap,
shared_extensions: Option<&'a mut crate::ExtensionMap>,
compositor: &'a mut Option<Box<dyn reovim_driver_layout::RootCompositor>>,
tabs: &'a mut crate::TabPageSet,
registers: &'a mut reovim_kernel::api::v1::RegisterBank,
clipboard_history: &'a mut reovim_kernel::api::v1::HistoryRing,
local_marks: &'a mut reovim_kernel::api::v1::MarkBank,
jumplist: &'a mut reovim_kernel::api::v1::Jumplist,
active_buffer: &'a mut Option<reovim_kernel::api::v1::BufferId>,
kernel: &'a KernelContext,
executor: &'a dyn CommandExecutor,
screen: Rect,
changes: StateChanges,
signals: Vec<RuntimeSignal>,
command_depth: usize,
cursor_snapshot: Option<(u32, u32)>,
}
impl<'a> SessionRuntime<'a> {
#[allow(clippy::needless_pass_by_value)] pub fn new(
session: &'a mut Session,
client: crate::ClientContext<'a>,
kernel: &'a KernelContext,
executor: &'a dyn CommandExecutor,
) -> Self {
let screen = {
let (width, height) = *client.terminal_size;
Rect::new(0, 0, width, height)
};
#[allow(clippy::cast_possible_truncation)]
let cursor_snapshot = client
.windows
.active()
.map(|w| (w.cursor.line as u32, w.cursor.column as u32));
Self {
owner: None,
session,
mode_stack: client.mode_stack,
windows: client.windows,
extensions: client.extensions,
shared_extensions: None,
compositor: client.compositor,
tabs: client.tabs,
registers: client.registers,
clipboard_history: client.clipboard_history,
local_marks: client.local_marks,
jumplist: client.jumplist,
active_buffer: client.active_buffer,
kernel,
executor,
screen,
changes: StateChanges::new(),
signals: Vec::new(),
command_depth: 0,
cursor_snapshot,
}
}
#[allow(clippy::needless_pass_by_value)] pub fn with_owner(
owner: crate::ClientId,
session: &'a mut Session,
client: crate::ClientContext<'a>,
kernel: &'a KernelContext,
executor: &'a dyn CommandExecutor,
) -> Self {
let screen = {
let (width, height) = *client.terminal_size;
Rect::new(0, 0, width, height)
};
#[allow(clippy::cast_possible_truncation)]
let cursor_snapshot = client
.windows
.active()
.map(|w| (w.cursor.line as u32, w.cursor.column as u32));
Self {
owner: Some(owner),
session,
mode_stack: client.mode_stack,
windows: client.windows,
extensions: client.extensions,
shared_extensions: None,
compositor: client.compositor,
tabs: client.tabs,
registers: client.registers,
clipboard_history: client.clipboard_history,
local_marks: client.local_marks,
jumplist: client.jumplist,
active_buffer: client.active_buffer,
kernel,
executor,
screen,
changes: StateChanges::new(),
signals: Vec::new(),
command_depth: 0,
cursor_snapshot,
}
}
#[must_use]
pub const fn owner(&self) -> Option<crate::ClientId> {
self.owner
}
#[must_use]
pub const fn with_shared_extensions(mut self, extensions: &'a mut crate::ExtensionMap) -> Self {
self.shared_extensions = Some(extensions);
self
}
pub fn signal(&mut self, signal: RuntimeSignal) {
self.signals.push(signal);
}
pub fn take_signals(&mut self) -> Vec<RuntimeSignal> {
std::mem::take(&mut self.signals)
}
#[must_use]
pub fn has_compositor(&self) -> bool {
self.compositor.is_some()
}
#[must_use]
pub const fn session(&self) -> &Session {
self.session
}
pub const fn session_mut(&mut self) -> &mut Session {
self.session
}
#[must_use]
pub const fn kernel(&self) -> &KernelContext {
self.kernel
}
pub fn with_buffer_read<F, R>(&self, buffer: BufferId, f: F) -> Option<R>
where
F: FnOnce(&reovim_kernel::api::v1::Buffer) -> R,
{
let buf_arc = self.kernel.buffers.get(buffer)?;
let buf = buf_arc.read();
Some(f(&buf))
}
pub fn record_global_option_change(&mut self, name: impl Into<String>, value: OptionValue) {
self.changes.record_global_option_change(name, value);
}
pub fn record_window_option_change(
&mut self,
name: impl Into<String>,
value: OptionValue,
window_id: WindowId,
) {
self.changes
.record_window_option_change(name, value, window_id);
}
pub fn record_buffer_modified(&mut self, buffer: BufferId) {
self.changes.record_buffer_modified(buffer);
}
#[must_use]
pub const fn windows(&self) -> &crate::WindowLayout {
self.windows
}
#[allow(clippy::missing_const_for_fn)] pub fn windows_mut(&mut self) -> &mut crate::WindowLayout {
self.windows
}
#[must_use]
pub const fn registers(&self) -> &reovim_kernel::api::v1::RegisterBank {
self.registers
}
#[allow(clippy::missing_const_for_fn)]
pub fn registers_mut(&mut self) -> &mut reovim_kernel::api::v1::RegisterBank {
self.registers
}
#[must_use]
pub const fn clipboard_history(&self) -> &reovim_kernel::api::v1::HistoryRing {
self.clipboard_history
}
#[allow(clippy::missing_const_for_fn)]
pub fn clipboard_history_mut(&mut self) -> &mut reovim_kernel::api::v1::HistoryRing {
self.clipboard_history
}
pub const fn kernel_and_registers(
&mut self,
) -> (
&KernelContext,
&mut reovim_kernel::api::v1::RegisterBank,
&mut reovim_kernel::api::v1::HistoryRing,
) {
(self.kernel, self.registers, self.clipboard_history)
}
#[must_use]
pub const fn local_marks(&self) -> &reovim_kernel::api::v1::MarkBank {
self.local_marks
}
#[allow(clippy::missing_const_for_fn)]
pub fn local_marks_mut(&mut self) -> &mut reovim_kernel::api::v1::MarkBank {
self.local_marks
}
#[must_use]
pub const fn jumplist(&self) -> &reovim_kernel::api::v1::Jumplist {
self.jumplist
}
#[allow(clippy::missing_const_for_fn)]
pub fn jumplist_mut(&mut self) -> &mut reovim_kernel::api::v1::Jumplist {
self.jumplist
}
}
impl ModeApi for SessionRuntime<'_> {
fn current_mode(&self) -> &ModeId {
self.mode_stack.current()
}
fn home_mode(&self) -> &ModeId {
self.mode_stack.home()
}
fn mode_depth(&self) -> usize {
self.mode_stack.depth()
}
fn is_mode_active(&self, mode: &ModeId) -> bool {
self.mode_stack.contains(mode)
}
fn mode_stack(&self) -> Vec<ModeId> {
self.mode_stack.as_slice().to_vec()
}
fn push_mode(&mut self, mode: ModeId, _ctx: TransitionContext) {
self.mode_stack.push(mode);
self.changes.record_mode_change();
}
fn pop_mode(&mut self, _result: Option<PopResult>) -> Result<(), ModeError> {
if self.mode_stack.depth() <= 1 {
return Err(ModeError::CannotPopHomeMode);
}
self.mode_stack.pop();
self.changes.record_mode_change();
Ok(())
}
fn set_mode(&mut self, mode: ModeId, _ctx: TransitionContext) {
self.mode_stack.set(mode);
self.changes.record_mode_change();
}
}
impl BufferApi for SessionRuntime<'_> {
fn active_buffer(&self) -> Option<BufferId> {
*self.active_buffer
}
fn set_active_buffer(&mut self, id: Option<BufferId>) {
*self.active_buffer = id;
}
fn buffer_line(&self, buffer: BufferId, line: usize) -> Option<String> {
self.kernel
.buffers
.get(buffer)
.and_then(|buf| buf.read().line(line).map(String::from))
}
fn buffer_line_count(&self, buffer: BufferId) -> Option<usize> {
self.kernel
.buffers
.get(buffer)
.map(|buf| buf.read().line_count())
}
fn buffer_line_len(&self, buffer: BufferId, line: usize) -> Option<usize> {
self.kernel
.buffers
.get(buffer)
.and_then(|buf| buf.read().line_len(line))
}
#[allow(clippy::significant_drop_tightening)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn buffer_text_range(
&self,
buffer: BufferId,
start: Position,
end: Position,
) -> Option<String> {
let buf_arc = self.kernel.buffers.get(buffer)?;
let buf = buf_arc.read();
let mut result = String::new();
if start.line == end.line {
if let Some(line) = buf.line(start.line) {
let char_len = line.chars().count();
let start_col = start.column.min(char_len);
let end_col = end.column.min(char_len);
if start_col < end_col {
let sb = char_col_to_byte(line, start_col);
let eb = char_col_to_byte(line, end_col);
result.push_str(&line[sb..eb]);
}
}
} else {
if let Some(line) = buf.line(start.line) {
let char_len = line.chars().count();
let start_col = start.column.min(char_len);
let sb = char_col_to_byte(line, start_col);
result.push_str(&line[sb..]);
result.push('\n');
}
for line_idx in (start.line + 1)..end.line {
if let Some(line) = buf.line(line_idx) {
result.push_str(line);
result.push('\n');
}
}
if let Some(line) = buf.line(end.line) {
let char_len = line.chars().count();
let end_col = end.column.min(char_len);
let eb = char_col_to_byte(line, end_col);
result.push_str(&line[..eb]);
}
}
Some(result)
}
fn buffer_content(&self, buffer: BufferId) -> Option<String> {
self.kernel
.buffers
.get(buffer)
.map(|buf| buf.read().content())
}
fn buffer_file_path(&self, buffer: BufferId) -> Option<String> {
self.kernel
.buffers
.get(buffer)
.and_then(|buf| buf.read().file_path().map(String::from))
}
fn is_buffer_modified(&self, buffer: BufferId) -> Option<bool> {
self.kernel
.buffers
.get(buffer)
.map(|buf| buf.read().is_modified())
}
fn set_buffer_modified(&mut self, buffer: BufferId, modified: bool) {
if let Some(buf) = self.kernel.buffers.get(buffer) {
buf.write().set_modified(modified);
}
}
fn insert_text(&mut self, buffer: BufferId, pos: Position, text: &str) {
if let Some(buf) = self.kernel.buffers.get(buffer) {
let cursor_before = self.windows().active().map_or_else(
|| Position::new(0, 0),
|w| Position::new(w.cursor.line, w.cursor.column),
);
let byte_offset = buf.read().position_to_byte(pos);
buf.write().insert_at(pos, text);
let cursor_after = cursor_before;
let edit = Edit::Insert {
position: pos,
text: text.to_string(),
};
self.record_edit_mine(buffer, vec![edit], cursor_before, cursor_after);
#[allow(clippy::cast_possible_truncation)]
{
use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
let modification = Modification::Insert {
start: (pos.line as u32, pos.column as u32),
text: text.to_string(),
start_byte: byte_offset,
};
self.kernel.event_bus.emit(BufferModified {
buffer_id: buffer.as_usize() as u64,
modification: modification.clone(),
});
self.changes
.record_buffer_modified_with_edit(buffer, modification);
}
}
}
fn delete_range(&mut self, buffer: BufferId, start: Position, end: Position) {
if let Some(buf) = self.kernel.buffers.get(buffer) {
let cursor_before = self.windows().active().map_or_else(
|| Position::new(0, 0),
|w| Position::new(w.cursor.line, w.cursor.column),
);
let byte_offset = buf.read().position_to_byte(start);
let deleted_text = {
let mut b = buf.write();
b.delete_range(start, end)
};
let cursor_after = cursor_before;
if deleted_text.is_empty() {
self.changes.record_buffer_modified(buffer);
} else {
let edit = Edit::Delete {
position: start,
text: deleted_text.clone(),
};
self.record_edit_mine(buffer, vec![edit], cursor_before, cursor_after);
#[allow(clippy::cast_possible_truncation)]
{
use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
let modification = Modification::Delete {
start: (start.line as u32, start.column as u32),
end: (end.line as u32, end.column as u32),
text: deleted_text,
start_byte: byte_offset,
};
self.kernel.event_bus.emit(BufferModified {
buffer_id: buffer.as_usize() as u64,
modification: modification.clone(),
});
self.changes
.record_buffer_modified_with_edit(buffer, modification);
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn replace_content(&mut self, buffer: BufferId, content: &str) {
if let Some(buf) = self.kernel.buffers.get(buffer) {
let cursor_before = self.windows().active().map_or_else(
|| Position::new(0, 0),
|w| Position::new(w.cursor.line, w.cursor.column),
);
let old_content = buf.read().content();
buf.write().set_content(content);
let edits = vec![
Edit::Delete {
position: Position::new(0, 0),
text: old_content,
},
Edit::Insert {
position: Position::new(0, 0),
text: content.to_string(),
},
];
self.record_edit_mine(buffer, edits, cursor_before, cursor_before);
#[allow(clippy::cast_possible_truncation)]
{
use reovim_kernel::api::v1::events::kernel::{BufferModified, Modification};
let modification = Modification::FullReplace;
self.kernel.event_bus.emit(BufferModified {
buffer_id: buffer.as_usize() as u64,
modification: modification.clone(),
});
self.changes
.record_buffer_modified_with_edit(buffer, modification);
}
}
}
fn create_buffer(&mut self, name: Option<&str>, content: &str) -> BufferId {
use reovim_kernel::api::v1::Buffer;
let mut buffer = Buffer::from_string(content);
if let Some(name) = name {
buffer.set_file_path(Some(name.to_string()));
}
let id = self.kernel.buffers.register(buffer);
self.changes.record_buffer_created(id);
id
}
fn delete_buffer(&mut self, buffer: BufferId) -> Result<(), BufferError> {
if self.kernel.buffers.count() <= 1 {
return Err(BufferError::CannotDeleteLastBuffer);
}
if self.kernel.buffers.unregister(buffer).is_err() {
return Err(BufferError::NotFound(buffer));
}
self.changes.record_buffer_deleted(buffer);
Ok(())
}
fn rename_buffer(&mut self, buffer: BufferId, new_name: &str) {
if let Some(buf) = self.kernel.buffers.get(buffer) {
buf.write().set_file_path(Some(new_name.to_string()));
self.changes
.record_buffer_renamed(buffer, new_name.to_string());
}
}
}
impl WindowApi for SessionRuntime<'_> {
fn active_window(&self) -> Option<WindowId> {
self.windows.active_id() }
fn cursor_position(&self) -> Option<Position> {
let window = self.windows().active()?;
Some(Position::new(window.cursor.line, window.cursor.column))
}
fn window_count(&self) -> usize {
self.windows.len() }
fn window_buffer(&self, window: WindowId) -> Option<BufferId> {
self.windows.get(window).and_then(|w| w.buffer_id) }
fn create_window(&mut self, buffer: Option<BufferId>) -> WindowId {
let mut window = Window::new();
window.buffer_id = buffer;
let id = window.id;
self.windows.add(window); self.changes.record_window_created(id);
id
}
fn close_window(&mut self, window: WindowId) -> Result<(), WindowError> {
if self.windows.len() <= 1 {
return Err(WindowError::CannotCloseLastWindow);
}
if self.windows.remove(window) {
self.changes.record_window_closed(window);
Ok(())
} else {
Err(WindowError::NotFound(window))
}
}
fn focus_window(&mut self, window: WindowId) -> Result<(), WindowError> {
if !self.windows.set_active(window) {
return Err(WindowError::NotFound(window));
}
self.changes.record_focus_change();
Ok(())
}
fn set_window_buffer(&mut self, window: WindowId, buffer: BufferId) -> Result<(), WindowError> {
if self.kernel.buffers.get(buffer).is_none() {
return Err(WindowError::BufferNotFound(buffer));
}
if let Some(w) = self.windows.get_mut(window) {
w.selection = None;
w.buffer_id = Some(buffer);
self.changes.window_changed = true;
Ok(())
} else {
Err(WindowError::NotFound(window))
}
}
fn set_active_selection(&mut self, selection: Option<Selection>) {
if let Some(w) = self.windows.active_mut() {
w.selection = selection;
}
}
fn active_selection(&self) -> Option<&Selection> {
self.windows.active().and_then(|w| w.selection.as_ref())
}
}
impl RegisterApi for SessionRuntime<'_> {
fn get_register(&self, name: Option<char>) -> Option<RegisterContent> {
match name {
Some(n) if n.is_ascii_digit() => self.clipboard_history.get_numbered(n),
_ => self.registers.get_by_name(name).cloned(),
}
}
fn set_register(&mut self, name: Option<char>, content: RegisterContent) {
match name {
Some(n) if n.is_ascii_digit() => {
}
_ => {
self.registers.set_by_name(name, content);
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl ClipboardApi for SessionRuntime<'_> {
fn copy_to_clipboard(&self, text: &str) -> bool {
if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
&& let Some(provider) = registry.get(&ClipboardKey::Default)
{
provider.copy_to_clipboard(text).is_ok()
} else {
false
}
}
fn paste_from_clipboard(&self) -> Option<String> {
if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
&& let Some(provider) = registry.get(&ClipboardKey::Default)
{
provider.paste_from_clipboard().ok().flatten()
} else {
None
}
}
fn copy_to_selection(&self, text: &str) -> bool {
if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
&& let Some(provider) = registry.get(&ClipboardKey::Default)
{
provider.copy_to_selection(text).is_ok()
} else {
false
}
}
fn paste_from_selection(&self) -> Option<String> {
if let Some(registry) = self.kernel.services.get::<ClipboardProviderRegistry>()
&& let Some(provider) = registry.get(&ClipboardKey::Default)
{
provider.paste_from_selection().ok().flatten()
} else {
None
}
}
}
impl SessionRuntime<'_> {
pub fn push_to_clipboard_history(&mut self, content: RegisterContent) {
self.clipboard_history.push(content);
}
pub fn store_register_with_sync(&mut self, register: Option<char>, content: RegisterContent) {
self.set_register(register, content.clone());
match register {
Some('+') => {
self.copy_to_clipboard(&content.text);
}
Some('*') => {
self.copy_to_selection(&content.text);
}
_ => {}
}
self.push_to_clipboard_history(content);
}
pub fn get_register_with_clipboard(&self, register: Option<char>) -> Option<RegisterContent> {
match register {
Some('+') => self
.paste_from_clipboard()
.map(RegisterContent::characterwise)
.or_else(|| self.get_register(register)),
Some('*') => self
.paste_from_selection()
.map(RegisterContent::characterwise)
.or_else(|| self.get_register(register)),
_ => self.get_register(register),
}
}
}
impl SessionRuntime<'_> {
fn apply_undo_edits(&self, buffer: BufferId, edits: &[Edit]) {
let Some(buf) = self.kernel.buffers.get(buffer) else {
return;
};
let mut buf = buf.write();
for edit in edits {
match edit {
Edit::Insert { position, text } => {
buf.insert_at(*position, text);
}
Edit::Delete { position, text } => {
buf.delete_at(*position, text.chars().count());
}
}
}
}
}
impl UndoApi for SessionRuntime<'_> {
#[cfg_attr(coverage_nightly, coverage(off))]
fn undo(&mut self, buffer: BufferId) -> Option<UndoResult> {
let undo_provider = self
.kernel
.services
.get::<UndoProviderRegistry>()?
.get(&UndoKey::Buffer)?;
let result = undo_provider.undo(buffer)?;
self.apply_undo_edits(buffer, &result.edits);
if let Some(window) = self.windows_mut().active_mut() {
window.cursor.line = result.cursor.line;
window.cursor.column = result.cursor.column;
}
self.changes.record_buffer_modified(buffer);
self.changes.record_cursor_move(buffer);
Some(result)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn redo(&mut self, buffer: BufferId) -> Option<UndoResult> {
let undo_provider = self
.kernel
.services
.get::<UndoProviderRegistry>()?
.get(&UndoKey::Buffer)?;
let result = undo_provider.redo(buffer)?;
self.apply_undo_edits(buffer, &result.edits);
if let Some(window) = self.windows_mut().active_mut() {
window.cursor.line = result.cursor.line;
window.cursor.column = result.cursor.column;
}
self.changes.record_buffer_modified(buffer);
self.changes.record_cursor_move(buffer);
Some(result)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn record_edit(
&mut self,
buffer: BufferId,
edits: Vec<Edit>,
cursor_before: Position,
cursor_after: Position,
) {
if let Some(undo_registry) = self.kernel.services.get::<UndoProviderRegistry>()
&& let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
{
undo_provider.record(buffer, edits, cursor_before, cursor_after);
}
}
fn can_undo(&self, buffer: BufferId) -> bool {
self.kernel
.services
.get::<UndoProviderRegistry>()
.and_then(|registry| registry.get(&UndoKey::Buffer))
.and_then(|provider| provider.get_tree(buffer))
.is_some_and(|tree| tree.can_undo())
}
fn can_redo(&self, buffer: BufferId) -> bool {
self.kernel
.services
.get::<UndoProviderRegistry>()
.and_then(|registry| registry.get(&UndoKey::Buffer))
.and_then(|provider| provider.get_tree(buffer))
.is_some_and(|tree| tree.can_redo())
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn undo_mine(&mut self, buffer: BufferId) -> Option<UndoResult> {
let client_id = self.owner?.as_usize();
let undo_provider = self
.kernel
.services
.get::<UndoProviderRegistry>()?
.get(&UndoKey::Buffer)?;
let result = undo_provider.undo_for_client(buffer, client_id)?;
self.apply_undo_edits(buffer, &result.edits);
if let Some(window) = self.windows_mut().active_mut() {
window.cursor.line = result.cursor.line;
window.cursor.column = result.cursor.column;
}
self.changes.record_buffer_modified(buffer);
self.changes.record_cursor_move(buffer);
Some(result)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn redo_mine(&mut self, buffer: BufferId) -> Option<UndoResult> {
let client_id = self.owner?.as_usize();
let undo_provider = self
.kernel
.services
.get::<UndoProviderRegistry>()?
.get(&UndoKey::Buffer)?;
let result = undo_provider.redo_for_client(buffer, client_id)?;
self.apply_undo_edits(buffer, &result.edits);
if let Some(window) = self.windows_mut().active_mut() {
window.cursor.line = result.cursor.line;
window.cursor.column = result.cursor.column;
}
self.changes.record_buffer_modified(buffer);
self.changes.record_cursor_move(buffer);
Some(result)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn record_edit_mine(
&mut self,
buffer: BufferId,
edits: Vec<Edit>,
cursor_before: Position,
cursor_after: Position,
) {
if let Some(client_id) = self.owner {
if let Some(undo_registry) = self.kernel.services.get::<UndoProviderRegistry>()
&& let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
{
undo_provider.record_for_client(
buffer,
client_id.as_usize(),
edits,
cursor_before,
cursor_after,
);
}
} else {
self.record_edit(buffer, edits, cursor_before, cursor_after);
}
}
}
impl CommandApi for SessionRuntime<'_> {
fn execute_command(&mut self, cmd: CommandId, ctx: CommandContext) -> CommandResult {
if self.command_depth >= 16 {
return CommandResult::Error(
"command recursion limit exceeded (max depth: 16)".to_string(),
);
}
self.command_depth += 1;
let handle = self.executor.get_handle(&cmd);
let result = handle.map_or_else(
|| CommandResult::Error(format!("command not found: {cmd:?}")),
|handle| handle.execute(self, &ctx),
);
self.command_depth -= 1;
result
}
}
impl ExtensionApi for SessionRuntime<'_> {
fn ext<T: SessionExtension>(&self) -> Option<&T> {
self.extensions.get::<T>()
}
fn ext_mut<T: SessionExtension>(&mut self) -> &mut T {
self.extensions.get_or_insert::<T>()
}
fn shared_ext<T: SessionExtension>(&self) -> Option<&T> {
self.shared_extensions.as_ref().and_then(|m| m.get::<T>())
}
fn shared_ext_mut<T: SessionExtension>(&mut self) -> Option<&mut T> {
self.shared_extensions
.as_mut()
.map(|m| m.get_or_insert::<T>())
}
}
impl ChangeTracker for SessionRuntime<'_> {
fn take_changes(&mut self) -> StateChanges {
std::mem::take(&mut self.changes)
}
fn record_cursor_move(&mut self, buffer: BufferId) {
#[allow(clippy::cast_possible_truncation)]
if let Some(window) = self.windows.active() {
let to = (window.cursor.line as u32, window.cursor.column as u32);
let from = self.cursor_snapshot.unwrap_or(to);
self.kernel.event_bus.emit(CursorMoved {
buffer_id: buffer.as_usize() as u64,
from,
to,
});
self.cursor_snapshot = Some(to);
}
self.changes.record_cursor_move(buffer);
if let Some(window) = self.windows.active_mut()
&& let Some(ref mut sel) = window.selection
{
sel.end = Position::new(window.cursor.line, window.cursor.column + 1);
self.changes.record_selection_change(buffer);
}
}
fn record_selection_change(&mut self, buffer: BufferId) {
self.changes.record_selection_change(buffer);
}
}
const fn to_kernel_split_direction(dir: SplitDirection) -> KernelSplitDirection {
match dir {
SplitDirection::Horizontal => KernelSplitDirection::Horizontal,
SplitDirection::Vertical => KernelSplitDirection::Vertical,
}
}
impl SessionRuntime<'_> {
fn emit_layout_event(&self, kind: LayoutChangeKind) {
let (window_count, focused_window) = self
.compositor
.as_ref()
.map_or((0, None), |c| (c.window_count(), c.focused().map(|id| id.as_usize() as u64)));
self.kernel.event_bus.emit(LayoutChanged {
kind,
window_count,
focused_window,
});
}
}
impl CompositorApi for SessionRuntime<'_> {
fn navigate(&self, direction: NavigateDirection) -> Result<WindowId, CompositorError> {
let compositor = self
.compositor
.as_ref()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer
.navigate_tiled(from, direction)
.ok_or(CompositorError::NoNeighbor(direction))
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn split(&mut self, direction: SplitDirection) -> Result<WindowId, CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
let new_window = layer
.split_tiled(from, direction)
.ok_or(CompositorError::NotEnoughRoom)?;
if let Some(source) = self.windows.get(from) {
let win = Window::split_from(new_window, source);
self.windows.add(win);
}
if let Some(compositor) = self.compositor.as_mut()
&& let Some(active) = compositor.active_layer()
&& let Some(layer) = compositor.layer_compositor_mut(active)
{
layer.set_focus(from);
}
self.windows.set_active(from);
self.changes.record_window_created(new_window);
self.emit_layout_event(LayoutChangeKind::Split {
new_window: new_window.as_usize() as u64,
direction: to_kernel_split_direction(direction),
});
Ok(new_window)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn close_current_window(&mut self) -> Result<WindowId, CompositorError> {
use reovim_driver_layout::Zone;
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
if layer.windows_in_zone(Zone::Tiled).len() <= 1 {
return Err(CompositorError::CannotCloseLastWindow);
}
let neighbor = layer
.close_tiled(current)
.ok_or(CompositorError::CannotCloseLastWindow)?;
self.windows.remove(current);
self.windows.set_active(neighbor);
self.changes.record_window_closed(current);
self.emit_layout_event(LayoutChangeKind::Close {
closed_window: current.as_usize() as u64,
new_focus: Some(neighbor.as_usize() as u64),
});
Ok(neighbor)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn close_others(&mut self) -> Result<(), CompositorError> {
use reovim_driver_layout::Zone;
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
let windows: Vec<WindowId> = layer
.windows_in_zone(Zone::Tiled)
.into_iter()
.filter(|&w| w != current)
.collect();
for window in &windows {
layer.close_tiled(*window);
self.windows.remove(*window);
self.changes.record_window_closed(*window);
}
if let Some(&last_closed) = windows.last() {
self.emit_layout_event(LayoutChangeKind::Close {
closed_window: last_closed.as_usize() as u64,
new_focus: Some(current.as_usize() as u64),
});
}
Ok(())
}
fn resize(&mut self, direction: NavigateDirection, delta: i16) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer.resize_tiled(current, direction, delta);
self.changes.window_changed = true;
self.emit_layout_event(LayoutChangeKind::Resize {
window: current.as_usize() as u64,
});
Ok(())
}
fn equalize(&mut self) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
layer.equalize_tiled();
self.changes.window_changed = true;
self.emit_layout_event(LayoutChangeKind::Equalize);
Ok(())
}
fn cycle(&self, forward: bool) -> Result<WindowId, CompositorError> {
let compositor = self
.compositor
.as_ref()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let from = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer
.cycle_tiled(from, forward)
.ok_or(CompositorError::NoFocusedWindow)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn focus(&mut self, window: WindowId) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let previous_focus = compositor.focused();
compositor.set_focus(window);
self.windows.set_active(window);
if let Some(buffer_id) = self.windows.active().and_then(|w| w.buffer_id) {
*self.active_buffer = Some(buffer_id);
}
self.changes.record_focus_change();
if previous_focus != Some(window) {
self.emit_layout_event(LayoutChangeKind::Focus {
from: previous_focus.map(|w| w.as_usize() as u64),
to: window.as_usize() as u64,
});
}
Ok(())
}
fn focused_window(&self) -> Option<WindowId> {
self.compositor.as_ref()?.focused()
}
fn compositor_window_count(&self) -> usize {
self.compositor.as_ref().map_or(0, |c| c.window_count())
}
fn arrange(&self, screen: Rect) -> Vec<WindowPlacement> {
self.compositor
.as_ref()
.map_or_else(Vec::new, |c| c.composite(screen).placements)
}
fn active_layer(&self) -> Option<LayerId> {
self.compositor.as_ref()?.active_layer()
}
fn set_screen(&mut self, screen: Rect) {
self.screen = screen;
if let Some(compositor) = self.compositor.as_mut() {
compositor.set_screen(screen);
}
}
fn toggle_float(&mut self) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer.toggle_float(current);
self.changes.window_changed = true;
Ok(())
}
fn raise_float(&mut self) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer.raise_float(current);
Ok(())
}
fn lower_float(&mut self) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let current = layer.focused().ok_or(CompositorError::NoFocusedWindow)?;
layer.lower_float(current);
Ok(())
}
fn show_overlay(
&mut self,
constraints: OverlayConstraints,
) -> Result<WindowId, CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
let id = layer.show_overlay(constraints);
Ok(id)
}
fn hide_overlay(&mut self, window: WindowId) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
layer.hide_overlay(window);
Ok(())
}
fn resize_overlay(
&mut self,
window: WindowId,
width: u16,
height: u16,
) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
layer.resize_overlay(window, width, height);
Ok(())
}
fn hide_all_overlays(&mut self) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let layer = compositor
.layer_compositor_mut(active)
.ok_or(CompositorError::LayerNotFound(active))?;
layer.hide_all_overlays();
Ok(())
}
fn set_active_layer_opacity(&mut self, opacity: f32) -> Result<(), CompositorError> {
let compositor = self
.compositor
.as_mut()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
compositor.set_layer_opacity(active, opacity.clamp(0.0, 1.0));
self.changes.window_changed = true;
Ok(())
}
fn active_layer_opacity(&self) -> Result<f32, CompositorError> {
let compositor = self
.compositor
.as_ref()
.ok_or(CompositorError::NoActiveLayer)?;
let active = compositor
.active_layer()
.ok_or(CompositorError::NoActiveLayer)?;
let opacity = compositor
.layers()
.iter()
.find(|l| l.id == active)
.map_or(1.0, |l| l.opacity);
Ok(opacity)
}
fn adjust_active_layer_opacity(&mut self, delta: f32) -> Result<f32, CompositorError> {
let current = self.active_layer_opacity()?;
let new_opacity = (current + delta).clamp(0.0, 1.0);
self.set_active_layer_opacity(new_opacity)?;
Ok(new_opacity)
}
fn tab_new(&mut self) -> Result<TabId, CompositorError> {
let id = self.tabs.new_tab();
self.changes.window_changed = true;
Ok(id)
}
fn tab_close(&mut self) -> Result<(), CompositorError> {
if self.tabs.close_tab() {
self.changes.window_changed = true;
Ok(())
} else {
Err(CompositorError::CannotCloseLastTab)
}
}
fn tab_next(&mut self) -> Result<TabId, CompositorError> {
let id = self.tabs.next_tab();
self.changes.window_changed = true;
Ok(id)
}
fn tab_prev(&mut self) -> Result<TabId, CompositorError> {
let id = self.tabs.prev_tab();
self.changes.window_changed = true;
Ok(id)
}
fn tab_goto(&mut self, index: usize) -> Result<TabId, CompositorError> {
self.tabs
.goto_tab(index)
.ok_or_else(|| CompositorError::TabNotFound(TabId::from_raw(index)))
.inspect(|_| {
self.changes.window_changed = true;
})
}
fn tab_count(&self) -> usize {
self.tabs.tab_count()
}
fn active_tab_id(&self) -> Option<TabId> {
Some(self.tabs.active_tab_id())
}
}
fn char_col_to_byte(line: &str, col: usize) -> usize {
line.char_indices().nth(col).map_or(line.len(), |(b, _)| b)
}
#[cfg(test)]
#[path = "runtime_tests.rs"]
mod tests;