1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
//! Scroll-sync orchestrators on `Editor`.
//!
//! - `ensure_active_tab_visible` — adjusts a split's tab-bar scroll offset
//! so the active tab is on screen.
//! - `sync_scroll_groups` — when splits share a scroll group (e.g. for
//! side-by-side diffs), keep their viewports in lockstep.
//! - `pre_sync_ensure_visible` — pre-sync hook that ensures the active
//! split's cursor is on screen so the scroll-group sync uses a valid
//! anchor.
use crate::model::event::{BufferId, LeafId, SplitId};
impl crate::app::window::Window {
/// Ensure the active tab in a split is visible by adjusting its
/// scroll offset. Pure window-state mutation: split tree +
/// view_states + buffer_metadata + composite_buffers + grouped_subtrees
/// all live on `Window`.
pub fn ensure_active_tab_visible(
&mut self,
split_id: LeafId,
active_buffer: BufferId,
available_width: u16,
) {
tracing::debug!(
"ensure_active_tab_visible called: split={:?}, buffer={:?}, width={}",
split_id,
active_buffer,
available_width
);
let group_names: std::collections::HashMap<LeafId, String> = self
.grouped_subtrees
.iter()
.filter_map(|(leaf_id, node)| {
if let crate::view::split::SplitNode::Grouped { name, .. } = node {
Some((*leaf_id, name.clone()))
} else {
None
}
})
.collect();
let metadata = &self.buffer_metadata;
let composites = &self.composite_buffers;
self.buffers.with_all_mut(|buffer_map, _mgr, vs_map| {
let Some(view_state) = vs_map.get_mut(&split_id) else {
return;
};
let split_buffers = view_state.open_buffers.clone();
let (tab_widths, rendered_targets) = crate::view::ui::tabs::calculate_tab_widths(
&split_buffers,
buffer_map,
metadata,
composites,
&group_names,
);
let total_tabs_width: usize = tab_widths.iter().sum();
let max_visible_width = available_width as usize;
let active_target = view_state.active_target();
let active_target = if matches!(active_target, crate::view::split::TabTarget::Buffer(_))
{
crate::view::split::TabTarget::Buffer(active_buffer)
} else {
active_target
};
let active_tab_index = rendered_targets.iter().position(|t| *t == active_target);
let active_width_index =
active_tab_index.map(|buf_idx| if buf_idx == 0 { 0 } else { buf_idx * 2 });
let old_offset = view_state.tab_scroll_offset;
let new_scroll_offset = if let Some(idx) = active_width_index {
crate::view::ui::tabs::scroll_to_show_tab(
&tab_widths,
idx,
view_state.tab_scroll_offset,
max_visible_width,
)
} else {
view_state
.tab_scroll_offset
.min(total_tabs_width.saturating_sub(max_visible_width))
};
tracing::debug!(
" -> offset: {} -> {} (idx={:?}, max_width={}, total={})",
old_offset,
new_scroll_offset,
active_width_index,
max_visible_width,
total_tabs_width
);
view_state.tab_scroll_offset = new_scroll_offset;
});
}
/// Synchronize viewports for all scroll-sync groups in this window.
///
/// For each registered group containing the active split, derive the
/// active split's top line from its viewport and project it onto the
/// paired split via the group's mapping. Then, when same-buffer
/// scroll sync is enabled, also mirror the active split's `top_byte`
/// onto every other split that shows the same buffer (and isn't in
/// an explicit sync group). The bottom-edge case sets
/// `sync_scroll_to_end` so the render pass does the
/// soft-break-aware fix-up using view lines.
pub(super) fn sync_scroll_groups(&mut self) {
let (mgr, vs_map) = self
.buffers
.splits()
.expect("window must have a populated split layout");
let active_split = mgr.active_split();
let group_count = self.scroll_sync_manager.groups().len();
if group_count > 0 {
tracing::debug!(
"sync_scroll_groups: active_split={:?}, {} groups",
active_split,
group_count
);
}
let sync_info: Vec<_> = self
.scroll_sync_manager
.groups()
.iter()
.filter_map(|group| {
tracing::debug!(
"sync_scroll_groups: checking group {}, left={:?}, right={:?}",
group.id,
group.left_split,
group.right_split
);
if !group.contains_split(active_split.into()) {
tracing::debug!(
"sync_scroll_groups: active split {:?} not in group",
active_split
);
return None;
}
let active_top_byte = vs_map.get(&active_split)?.viewport.top_byte;
let active_buffer_id = mgr.buffer_for_split(active_split)?;
let buffer_state = self.buffers.get(&active_buffer_id)?;
let buffer_len = buffer_state.buffer.len();
let active_line = buffer_state.buffer.get_line_number(active_top_byte);
tracing::debug!(
"sync_scroll_groups: active_split={:?}, buffer_id={:?}, top_byte={}, buffer_len={}, active_line={}",
active_split,
active_buffer_id,
active_top_byte,
buffer_len,
active_line
);
let (other_split, other_line) = if group.is_left_split(active_split.into()) {
(group.right_split, group.left_to_right_line(active_line))
} else {
(group.left_split, group.right_to_left_line(active_line))
};
tracing::debug!(
"sync_scroll_groups: syncing other_split={:?} to line {}",
other_split,
other_line
);
Some((other_split, other_line))
})
.collect();
for (other_split, target_line) in sync_info {
let other_leaf = LeafId(other_split);
let buffer_id = self
.buffers
.splits()
.expect("window must have a populated split layout")
.0
.buffer_for_split(other_leaf);
if let Some(buffer_id) = buffer_id {
self.scroll_split_viewport_to(buffer_id, other_leaf, target_line, false);
}
}
let active_buffer_id = if self.same_buffer_scroll_sync {
self.buffers
.splits()
.expect("window must have a populated split layout")
.0
.buffer_for_split(active_split)
} else {
None
};
if let Some(active_buf_id) = active_buffer_id {
let (mgr, vs_map) = self
.buffers
.splits()
.expect("window must have a populated split layout");
let active_top_byte = vs_map.get(&active_split).map(|vs| vs.viewport.top_byte);
let active_viewport_height = vs_map
.get(&active_split)
.map(|vs| vs.viewport.visible_line_count())
.unwrap_or(0);
if let Some(top_byte) = active_top_byte {
let other_splits: Vec<_> = vs_map
.keys()
.filter(|&&s| {
s != active_split
&& mgr.buffer_for_split(s) == Some(active_buf_id)
&& !self.scroll_sync_manager.is_split_synced(s.into())
})
.copied()
.collect();
if !other_splits.is_empty() {
let at_bottom = if let Some(state) = self.buffers.get_mut(&active_buf_id) {
let mut iter = state.buffer.line_iterator(top_byte, 80);
let mut lines_remaining = 0;
while iter.next_line().is_some() {
lines_remaining += 1;
if lines_remaining > active_viewport_height {
break;
}
}
lines_remaining <= active_viewport_height
} else {
false
};
let (_, vs_map_mut) = self
.buffers
.splits_mut()
.expect("window must have a populated split layout");
for other_split in other_splits {
if let Some(view_state) = vs_map_mut.get_mut(&other_split) {
view_state.viewport.top_byte = top_byte;
view_state.viewport.sync_scroll_to_end = at_bottom;
}
}
}
}
}
}
/// Pre-sync ensure-visible hook for scroll-sync groups in this window.
///
/// When the active split is in a sync group we update its viewport
/// here (before `sync_scroll_groups`) so commands like `G` produce
/// the right scroll position that gets mirrored. The other split in
/// the group is then marked to skip `ensure_visible` during render
/// so the sync isn't undone. Same-buffer sync mirrors the same
/// "skip" mark across the other splits showing the same buffer.
pub(super) fn pre_sync_ensure_visible(&mut self, active_split: LeafId) {
let group_info = self
.scroll_sync_manager
.find_group_for_split(active_split.into())
.map(|g| (g.left_split, g.right_split));
if let Some((left_split, right_split)) = group_info {
let buffer_id = self
.buffers
.splits()
.expect("window must have a populated split layout")
.0
.buffer_for_split(active_split);
if let Some(buffer_id) = buffer_id {
self.ensure_cursor_visible_for_split(buffer_id, active_split);
}
let active_sid: SplitId = active_split.into();
let other_split: SplitId = if active_sid == left_split {
right_split
} else {
left_split
};
if let Some((_, vs_map)) = self.buffers.splits_mut() {
if let Some(view_state) = vs_map.get_mut(&LeafId(other_split)) {
view_state.viewport.set_skip_ensure_visible();
tracing::debug!(
"pre_sync_ensure_visible: marked other split {:?} to skip ensure_visible",
other_split
);
}
}
}
if !self.same_buffer_scroll_sync {
return;
}
let active_buf_id = match self
.buffers
.splits()
.expect("window must have a populated split layout")
.0
.buffer_for_split(active_split)
{
Some(b) => b,
None => return,
};
let other_same_buffer_splits: Vec<_> = {
let (mgr, vs_map) = self
.buffers
.splits()
.expect("window must have a populated split layout");
vs_map
.keys()
.filter(|&&s| {
s != active_split
&& mgr.buffer_for_split(s) == Some(active_buf_id)
&& !self.scroll_sync_manager.is_split_synced(s.into())
})
.copied()
.collect()
};
if let Some((_, vs_map)) = self.buffers.splits_mut() {
for other_split in other_same_buffer_splits {
if let Some(view_state) = vs_map.get_mut(&other_split) {
view_state.viewport.set_skip_ensure_visible();
}
}
}
}
}