Skip to content

Commit 2db0adc

Browse files
committed
fix(tui): reserve right panels and save active edits
Reserve both left and right plugin panel widths before laying out editor windows so right-side panels do not draw over editor content. Commit active insert transactions before save checkpoints are marked clean, then reopen insert transactions so continued insert-mode edits keep normal grouping.
1 parent 68e0ec3 commit 2db0adc

3 files changed

Lines changed: 143 additions & 26 deletions

File tree

src/editor.rs

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -758,11 +758,14 @@ impl Editor {
758758

759759
fn resize_window_layout(&mut self, terminal_size: (usize, usize)) {
760760
self.sync_to_window();
761-
let reserved_left = self.reserved_panel_left_width();
761+
let (reserved_left, reserved_right) = self.reserved_panel_widths(terminal_size.0);
762762
self.window_manager.resize_with_origin(
763763
Point::new(reserved_left, 0),
764764
(
765-
terminal_size.0.saturating_sub(reserved_left),
765+
terminal_size
766+
.0
767+
.saturating_sub(reserved_left)
768+
.saturating_sub(reserved_right),
766769
terminal_size.1,
767770
),
768771
);
@@ -771,20 +774,26 @@ impl Editor {
771774

772775
fn apply_panel_layout(&mut self) {
773776
self.sync_to_window();
774-
let reserved_left = self.reserved_panel_left_width();
777+
let (reserved_left, reserved_right) = self.reserved_panel_widths(self.size.0 as usize);
775778
self.window_manager.resize_with_origin(
776779
Point::new(reserved_left, 0),
777780
(
778-
(self.size.0 as usize).saturating_sub(reserved_left),
781+
(self.size.0 as usize)
782+
.saturating_sub(reserved_left)
783+
.saturating_sub(reserved_right),
779784
self.size.1 as usize,
780785
),
781786
);
782787
}
783788

784-
fn reserved_panel_left_width(&self) -> usize {
785-
self.panel_manager
786-
.reserved_left_width()
787-
.min((self.size.0 as usize).saturating_sub(10))
789+
fn reserved_panel_widths(&self, terminal_width: usize) -> (usize, usize) {
790+
let max_reserved = terminal_width.saturating_sub(10);
791+
let reserved_left = self.panel_manager.reserved_left_width().min(max_reserved);
792+
let reserved_right = self
793+
.panel_manager
794+
.reserved_right_width()
795+
.min(max_reserved.saturating_sub(reserved_left));
796+
(reserved_left, reserved_right)
788797
}
789798

790799
fn indentation(&self) -> Indentation {
@@ -3059,28 +3068,38 @@ impl Editor {
30593068
}
30603069
self.draw_line(buffer);
30613070
}
3062-
Action::Save => match self.current_buffer_mut().save() {
3063-
Ok(msg) => {
3064-
// TODO: use last_message instead of last_error
3065-
self.last_error = Some(msg);
3071+
Action::Save => {
3072+
let resume_insert_transaction = self.commit_active_transaction_before_save();
3073+
let save_result = self.current_buffer_mut().save();
3074+
self.resume_insert_transaction_after_save(resume_insert_transaction);
30663075

3067-
// Notify plugins about file save
3068-
if let Some(file) = &self.current_buffer().file {
3069-
let save_info = serde_json::json!({
3070-
"file": file,
3071-
"buffer_index": self.current_buffer_index
3072-
});
3073-
self.plugin_registry
3074-
.notify(runtime, "file:saved", save_info)
3075-
.await?;
3076+
match save_result {
3077+
Ok(msg) => {
3078+
// TODO: use last_message instead of last_error
3079+
self.last_error = Some(msg);
3080+
3081+
// Notify plugins about file save
3082+
if let Some(file) = &self.current_buffer().file {
3083+
let save_info = serde_json::json!({
3084+
"file": file,
3085+
"buffer_index": self.current_buffer_index
3086+
});
3087+
self.plugin_registry
3088+
.notify(runtime, "file:saved", save_info)
3089+
.await?;
3090+
}
3091+
}
3092+
Err(e) => {
3093+
self.last_error = Some(e.to_string());
30763094
}
30773095
}
3078-
Err(e) => {
3079-
self.last_error = Some(e.to_string());
3080-
}
3081-
},
3096+
}
30823097
Action::SaveAs(new_file_name) => {
3083-
match self.current_buffer_mut().save_as(new_file_name) {
3098+
let resume_insert_transaction = self.commit_active_transaction_before_save();
3099+
let save_result = self.current_buffer_mut().save_as(new_file_name);
3100+
self.resume_insert_transaction_after_save(resume_insert_transaction);
3101+
3102+
match save_result {
30843103
Ok(msg) => {
30853104
// TODO: use last_message instead of last_error
30863105
self.last_error = Some(msg);
@@ -4249,6 +4268,20 @@ impl Editor {
42494268
self.current_buffer().undo_history.is_transaction_active()
42504269
}
42514270

4271+
fn commit_active_transaction_before_save(&mut self) -> bool {
4272+
let was_active = self.transaction_active();
4273+
if was_active {
4274+
self.commit_transaction(self.cursor_snapshot());
4275+
}
4276+
was_active
4277+
}
4278+
4279+
fn resume_insert_transaction_after_save(&mut self, was_active: bool) {
4280+
if was_active && self.is_insert() && !self.transaction_active() {
4281+
self.begin_transaction("insert");
4282+
}
4283+
}
4284+
42524285
fn replace_range(&mut self, range: TextRange, new_text: &str) {
42534286
let old_text = self.current_buffer().text_in_range(range);
42544287
if old_text == new_text {
@@ -4637,6 +4670,20 @@ impl Editor {
46374670
self.window_manager.active_window_id()
46384671
}
46394672

4673+
#[doc(hidden)]
4674+
pub fn test_active_window_bounds(&self) -> Option<(Point, (usize, usize))> {
4675+
self.window_manager
4676+
.active_window()
4677+
.map(|window| (window.position, window.size))
4678+
}
4679+
4680+
#[doc(hidden)]
4681+
pub fn test_create_panel(&mut self, id: &str, config: plugin::PanelConfig) {
4682+
self.panel_manager.create_panel(id.to_string(), config);
4683+
self.apply_panel_layout();
4684+
self.sync_with_window();
4685+
}
4686+
46404687
#[doc(hidden)]
46414688
pub fn test_render_cursor_position(&self) -> Option<(usize, usize)> {
46424689
self.render_cursor_position()

src/plugin/panel.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ impl PanelManager {
177177
.sum()
178178
}
179179

180+
pub fn reserved_right_width(&self) -> usize {
181+
self.panels
182+
.values()
183+
.filter(|panel| panel.config.side == PanelSide::Right)
184+
.map(|panel| panel.config.width.saturating_add(1))
185+
.sum()
186+
}
187+
180188
pub fn handle_focused_key(&mut self, action: &str, height: usize) -> Option<PanelEvent> {
181189
let focused = self.focused.clone()?;
182190
let panel = self.panels.get_mut(&focused)?;
@@ -317,6 +325,21 @@ mod tests {
317325
assert_eq!(manager.reserved_left_width(), 25);
318326
}
319327

328+
#[test]
329+
fn right_panels_reserve_width_with_separator() {
330+
let mut manager = PanelManager::default();
331+
manager.create_panel(
332+
"tree".to_string(),
333+
PanelConfig {
334+
side: PanelSide::Right,
335+
width: 24,
336+
title: None,
337+
},
338+
);
339+
340+
assert_eq!(manager.reserved_right_width(), 25);
341+
}
342+
320343
#[test]
321344
fn focused_panel_moves_selection() {
322345
let mut manager = PanelManager::default();

tests/editing.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use red::{
66
config::Config,
77
editor::{Action, Content, Editor, Mode},
88
lsp::LspClient,
9+
plugin::{PanelConfig, PanelSide},
910
theme::Theme,
1011
};
1112
use std::{
@@ -767,6 +768,34 @@ async fn test_dirty_checkpoint_moves_after_save() {
767768
let _ = fs::remove_file(path);
768769
}
769770

771+
#[tokio::test]
772+
async fn test_save_during_insert_keeps_saved_buffer_clean_on_escape() {
773+
let path = temp_file_path("dirty-save-insert");
774+
fs::write(&path, "abc").unwrap();
775+
776+
let buffer = Buffer::new(Some(path.clone()), "abc".to_string());
777+
let mut harness = EditorHarness::with_buffer(buffer);
778+
779+
harness
780+
.execute_action(Action::EnterMode(Mode::Insert))
781+
.await
782+
.unwrap();
783+
harness.type_text("z").await.unwrap();
784+
assert!(harness.is_dirty());
785+
786+
harness.execute_action(Action::Save).await.unwrap();
787+
assert!(!harness.is_dirty());
788+
789+
harness
790+
.execute_action(Action::EnterMode(Mode::Normal))
791+
.await
792+
.unwrap();
793+
harness.assert_buffer_contents("zabc");
794+
assert!(!harness.is_dirty());
795+
796+
let _ = fs::remove_file(path);
797+
}
798+
770799
#[tokio::test]
771800
async fn test_dirty_remains_after_undoing_past_saved_revision() {
772801
let path = temp_file_path("dirty-past-save");
@@ -789,6 +818,24 @@ async fn test_dirty_remains_after_undoing_past_saved_revision() {
789818
let _ = fs::remove_file(path);
790819
}
791820

821+
#[test]
822+
fn test_right_panel_reserves_editor_window_width() {
823+
let mut harness = EditorHarness::with_content("abcdef");
824+
825+
harness.editor.test_create_panel(
826+
"tree",
827+
PanelConfig {
828+
side: PanelSide::Right,
829+
width: 20,
830+
title: None,
831+
},
832+
);
833+
834+
let (position, size) = harness.editor.test_active_window_bounds().unwrap();
835+
assert_eq!(position.x, 0);
836+
assert_eq!(size.0, 59);
837+
}
838+
792839
#[tokio::test]
793840
async fn test_dirty_isolated_per_buffer() {
794841
let lsp = Box::new(MockLsp) as Box<dyn LspClient + Send>;

0 commit comments

Comments
 (0)