feat: add editor tabs
This commit is contained in:
+733
-40
@@ -26,6 +26,7 @@ pub enum Input {
|
||||
Char(char),
|
||||
Ctrl(char),
|
||||
Alt(char),
|
||||
CtrlAlt(char),
|
||||
Paste(String),
|
||||
Escape,
|
||||
Enter,
|
||||
@@ -387,6 +388,23 @@ core_actions![
|
||||
false,
|
||||
true
|
||||
),
|
||||
("core.tab.new", Action::TabNew, &[Mode::Normal], true),
|
||||
(
|
||||
"core.tab.previous",
|
||||
Action::TabPrevious,
|
||||
&[Mode::Normal],
|
||||
true
|
||||
),
|
||||
("core.tab.next", Action::TabNext, &[Mode::Normal], true),
|
||||
("core.tab.close", Action::TabClose, &[Mode::Normal], true),
|
||||
(
|
||||
"core.tab.force_close",
|
||||
Action::TabForceClose,
|
||||
&[Mode::Normal],
|
||||
true,
|
||||
false,
|
||||
true
|
||||
),
|
||||
("core.insert", Action::Insert, &[Mode::Normal]),
|
||||
(
|
||||
"core.insert_first_nonblank",
|
||||
@@ -800,6 +818,20 @@ fn parse_key(key: &str) -> Result<Input, String> {
|
||||
return Ok(Input::Char(ch));
|
||||
}
|
||||
let invalid = || format!("Invalid binding key: {key:?}");
|
||||
if let Some((first, rest)) = key.split_once('+')
|
||||
&& let Some((second, name)) = rest.split_once('+')
|
||||
{
|
||||
let alt = |modifier: &str| {
|
||||
modifier.eq_ignore_ascii_case("alt") || modifier.eq_ignore_ascii_case("option")
|
||||
};
|
||||
if ((first.eq_ignore_ascii_case("ctrl") && alt(second))
|
||||
|| (alt(first) && second.eq_ignore_ascii_case("ctrl")))
|
||||
&& let Some(ch) = character(name).filter(char::is_ascii_alphabetic)
|
||||
{
|
||||
return Ok(Input::CtrlAlt(ch.to_ascii_lowercase()));
|
||||
}
|
||||
return Err(invalid());
|
||||
}
|
||||
let (modifier, name) = key
|
||||
.split_once('+')
|
||||
.map_or((None, key), |(modifier, name)| (Some(modifier), name));
|
||||
@@ -876,6 +908,7 @@ fn append_key(display: &mut String, input: &Input) {
|
||||
Input::Char(ch) => display.push(*ch),
|
||||
Input::Ctrl(ch) => write!(display, "Ctrl+{ch}").expect("writing to a String"),
|
||||
Input::Alt(ch) => write!(display, "Alt+{ch}").expect("writing to a String"),
|
||||
Input::CtrlAlt(ch) => write!(display, "Ctrl+Alt+{ch}").expect("writing to a String"),
|
||||
Input::Escape => display.push_str("Esc"),
|
||||
Input::Enter => display.push_str("Enter"),
|
||||
Input::Backspace => display.push_str("Backspace"),
|
||||
@@ -923,6 +956,8 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
(Input::Alt('j'), PaneFocusDown),
|
||||
(Input::Alt('k'), PaneFocusUp),
|
||||
(Input::Alt('l'), PaneFocusRight),
|
||||
(Input::CtrlAlt('h'), TabPrevious),
|
||||
(Input::CtrlAlt('l'), TabNext),
|
||||
] {
|
||||
bindings
|
||||
.entry(Mode::Normal)
|
||||
@@ -1170,6 +1205,26 @@ struct Pane {
|
||||
views: BTreeMap<usize, ViewState>,
|
||||
}
|
||||
|
||||
struct Tab {
|
||||
pane_id: usize,
|
||||
pane_layout: PaneTree,
|
||||
file_picker: Option<FilePicker>,
|
||||
diff: Option<DiffState>,
|
||||
parked_diff: Option<DiffState>,
|
||||
diff_owner: Option<usize>,
|
||||
diff_prompt_mode: Option<Mode>,
|
||||
diff_pane_bounds: [(Focus, PaneRect); 3],
|
||||
diff_bound: Option<(u64, usize, Focus, usize)>,
|
||||
diff_return_buffer: Option<usize>,
|
||||
outstanding_diff_request: Option<u64>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
fn diff(&self) -> Option<&DiffState> {
|
||||
self.diff.as_ref().or(self.parked_diff.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
struct Buffer {
|
||||
id: usize,
|
||||
diff_snapshot: Option<(u64, usize, Focus)>,
|
||||
@@ -1275,6 +1330,10 @@ pub struct Editor {
|
||||
next_pane_id: usize,
|
||||
panes: BTreeMap<usize, Pane>,
|
||||
pane_layout: PaneTree,
|
||||
tab_id: usize,
|
||||
next_tab_id: usize,
|
||||
tab_order: Vec<usize>,
|
||||
tabs: BTreeMap<usize, Tab>,
|
||||
pane_bounds: PaneRect,
|
||||
render_pane_focused: bool,
|
||||
diff_owner: Option<usize>,
|
||||
@@ -1344,6 +1403,10 @@ impl Editor {
|
||||
},
|
||||
)]),
|
||||
pane_layout: PaneTree::new(1),
|
||||
tab_id: 1,
|
||||
next_tab_id: 2,
|
||||
tab_order: vec![1],
|
||||
tabs: BTreeMap::new(),
|
||||
pane_bounds: PaneRect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -1464,6 +1527,131 @@ impl Editor {
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
pub fn tab_id(&self) -> usize {
|
||||
self.tab_id
|
||||
}
|
||||
|
||||
pub fn tab_index(&self) -> usize {
|
||||
self.tab_order
|
||||
.iter()
|
||||
.position(|id| *id == self.tab_id)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn tab_count(&self) -> usize {
|
||||
self.tab_order.len()
|
||||
}
|
||||
|
||||
pub fn tab_buffer(&self, index: usize) -> Option<BufferInfo<'_>> {
|
||||
let id = *self.tab_order.get(index)?;
|
||||
let pane = if id == self.tab_id {
|
||||
self.pane_id
|
||||
} else {
|
||||
self.tabs.get(&id)?.pane_id
|
||||
};
|
||||
self.buffer(self.pane_owned_buffer(pane))
|
||||
.map(|buffer| buffer.info(id == self.tab_id))
|
||||
}
|
||||
|
||||
pub(crate) fn tab_diff(&self, id: usize) -> Option<&DiffState> {
|
||||
if id == self.tab_id {
|
||||
self.workspace_diff()
|
||||
} else {
|
||||
self.tabs.get(&id)?.diff()
|
||||
}
|
||||
}
|
||||
|
||||
fn select_tab(&mut self, id: usize) {
|
||||
if id == self.tab_id {
|
||||
return;
|
||||
}
|
||||
let next = self.tabs.remove(&id).expect("selected tab exists");
|
||||
self.stash_view();
|
||||
let previous = Tab {
|
||||
pane_id: std::mem::replace(&mut self.pane_id, next.pane_id),
|
||||
pane_layout: std::mem::replace(&mut self.pane_layout, next.pane_layout),
|
||||
file_picker: std::mem::replace(&mut self.file_picker, next.file_picker),
|
||||
diff: std::mem::replace(&mut self.diff, next.diff),
|
||||
parked_diff: std::mem::replace(&mut self.parked_diff, next.parked_diff),
|
||||
diff_owner: std::mem::replace(&mut self.diff_owner, next.diff_owner),
|
||||
diff_pane_bounds: std::mem::replace(&mut self.diff_pane_bounds, next.diff_pane_bounds),
|
||||
diff_prompt_mode: std::mem::replace(&mut self.diff_prompt_mode, next.diff_prompt_mode),
|
||||
diff_bound: std::mem::replace(&mut self.diff_bound, next.diff_bound),
|
||||
diff_return_buffer: std::mem::replace(
|
||||
&mut self.diff_return_buffer,
|
||||
next.diff_return_buffer,
|
||||
),
|
||||
outstanding_diff_request: std::mem::replace(
|
||||
&mut self.outstanding_diff_request,
|
||||
next.outstanding_diff_request,
|
||||
),
|
||||
};
|
||||
self.tabs.insert(self.tab_id, previous);
|
||||
self.tab_id = id;
|
||||
let buffer_id = self.panes[&self.pane_id].buffer_id;
|
||||
if buffer_id != self.active.id {
|
||||
let buffer = self.parked.remove(&buffer_id).expect("tab buffer exists");
|
||||
let previous = std::mem::replace(&mut self.active, buffer);
|
||||
self.parked.insert(previous.id, previous);
|
||||
}
|
||||
self.load_view();
|
||||
}
|
||||
|
||||
fn new_tab(&mut self) {
|
||||
self.finish_pane_interaction();
|
||||
let buffer_id = self.active.id;
|
||||
let view = self.active.view.inherit();
|
||||
let pane_id = self.next_pane_id;
|
||||
self.next_pane_id += 1;
|
||||
self.panes.insert(
|
||||
pane_id,
|
||||
Pane {
|
||||
buffer_id,
|
||||
origin: None,
|
||||
views: BTreeMap::from([(buffer_id, view)]),
|
||||
},
|
||||
);
|
||||
let id = self.next_tab_id;
|
||||
self.next_tab_id += 1;
|
||||
self.tabs.insert(
|
||||
id,
|
||||
Tab {
|
||||
pane_id,
|
||||
pane_layout: PaneTree::new(pane_id),
|
||||
file_picker: None,
|
||||
diff: None,
|
||||
parked_diff: None,
|
||||
diff_owner: None,
|
||||
diff_pane_bounds: self.diff_pane_bounds,
|
||||
diff_prompt_mode: None,
|
||||
diff_bound: None,
|
||||
diff_return_buffer: None,
|
||||
outstanding_diff_request: None,
|
||||
},
|
||||
);
|
||||
self.tab_order.push(id);
|
||||
self.select_tab(id);
|
||||
self.normalize_targets();
|
||||
if self.active.view.mode == Mode::Insert {
|
||||
self.begin_insert();
|
||||
}
|
||||
}
|
||||
|
||||
fn adjacent_tab(&mut self, forward: bool) {
|
||||
self.finish_pane_interaction();
|
||||
let index = self.tab_index();
|
||||
let count = self.tab_count();
|
||||
let next = if forward {
|
||||
(index + 1) % count
|
||||
} else {
|
||||
(index + count - 1) % count
|
||||
};
|
||||
self.select_tab(self.tab_order[next]);
|
||||
if self.active.view.mode == Mode::Insert {
|
||||
self.begin_insert();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pane_id(&self) -> usize {
|
||||
self.pane_id
|
||||
}
|
||||
@@ -1543,6 +1731,22 @@ impl Editor {
|
||||
fn with_pane<R>(&mut self, id: usize, callback: impl FnOnce(&mut Self) -> R) -> R {
|
||||
assert!(self.panes.contains_key(&id), "rendered pane exists");
|
||||
let focused = self.pane_id;
|
||||
let tab = self.tab_id;
|
||||
let target_tab = if self.pane_layout.contains(id) {
|
||||
tab
|
||||
} else {
|
||||
*self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|(_, tab)| tab.pane_layout.contains(id))
|
||||
.map(|(id, _)| id)
|
||||
.expect("pane belongs to a tab")
|
||||
};
|
||||
let target_focused = if target_tab == tab {
|
||||
focused
|
||||
} else {
|
||||
self.tabs[&target_tab].pane_id
|
||||
};
|
||||
let interaction = (id != focused).then(|| {
|
||||
(
|
||||
self.palette.take(),
|
||||
@@ -1556,11 +1760,14 @@ impl Editor {
|
||||
std::mem::take(&mut self.g_pending),
|
||||
self.line_pending.take(),
|
||||
self.seek_pending.take(),
|
||||
std::mem::take(&mut self.message),
|
||||
)
|
||||
});
|
||||
self.select_tab(target_tab);
|
||||
self.select_pane(id);
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(self)));
|
||||
self.select_pane(focused);
|
||||
self.select_pane(target_focused);
|
||||
self.select_tab(tab);
|
||||
if let Some(interaction) = interaction {
|
||||
(
|
||||
self.palette,
|
||||
@@ -1574,6 +1781,7 @@ impl Editor {
|
||||
self.g_pending,
|
||||
self.line_pending,
|
||||
self.seek_pending,
|
||||
self.message,
|
||||
) = interaction;
|
||||
}
|
||||
match result {
|
||||
@@ -1679,22 +1887,104 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_owned_buffer(&self, id: usize) -> usize {
|
||||
if self.diff_owner == Some(id) {
|
||||
self.diff
|
||||
.as_ref()
|
||||
.or(self.parked_diff.as_ref())
|
||||
.and_then(|diff| diff.live_buffer)
|
||||
.or(self.diff_return_buffer)
|
||||
.unwrap_or(self.panes[&id].buffer_id)
|
||||
fn pane_buffers(&self, id: usize) -> [Option<usize>; 3] {
|
||||
let (diff, return_buffer) = if self.diff_owner == Some(id) {
|
||||
(self.workspace_diff(), self.diff_return_buffer)
|
||||
} else if let Some(tab) = self.tabs.values().find(|tab| tab.diff_owner == Some(id)) {
|
||||
(tab.diff(), tab.diff_return_buffer)
|
||||
} else {
|
||||
self.panes[&id].buffer_id
|
||||
(None, None)
|
||||
};
|
||||
[
|
||||
Some(self.panes[&id].buffer_id),
|
||||
diff.and_then(|diff| diff.live_buffer),
|
||||
return_buffer,
|
||||
]
|
||||
}
|
||||
|
||||
fn pane_owned_buffer(&self, id: usize) -> usize {
|
||||
let buffers = self.pane_buffers(id);
|
||||
buffers[1].or(buffers[2]).or(buffers[0]).unwrap()
|
||||
}
|
||||
|
||||
fn closing_buffers(&self, panes: &[usize]) -> Vec<usize> {
|
||||
let mut buffers = Vec::new();
|
||||
for pane in panes {
|
||||
for buffer in self.pane_buffers(*pane).into_iter().flatten() {
|
||||
if !buffers.contains(&buffer)
|
||||
&& !self.panes.keys().any(|other| {
|
||||
!panes.contains(other) && self.pane_buffers(*other).contains(&Some(buffer))
|
||||
})
|
||||
{
|
||||
buffers.push(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
buffers
|
||||
}
|
||||
|
||||
fn forget_buffer(&mut self, id: usize) {
|
||||
self.parked.remove(&id);
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.views.remove(&id);
|
||||
}
|
||||
self.jumps.retain(|jump| jump.buffer_id != id);
|
||||
self.jump_index = self.jump_index.min(self.jumps.len().saturating_sub(1));
|
||||
}
|
||||
|
||||
fn close_tab(&mut self, force: bool) {
|
||||
if self.tab_count() == 1 {
|
||||
if !force && self.any_dirty() {
|
||||
self.set_message("Unsaved changes; use :w or :tab.force_close");
|
||||
} else {
|
||||
self.quit = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let panes: Vec<_> = self
|
||||
.panes
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|id| self.pane_layout.contains(*id))
|
||||
.collect();
|
||||
let buffers = self.closing_buffers(&panes);
|
||||
if !force
|
||||
&& buffers
|
||||
.iter()
|
||||
.any(|id| self.buffer(*id).is_some_and(Buffer::dirty))
|
||||
{
|
||||
self.set_message("Unsaved changes; use :w or :tab.force_close");
|
||||
return;
|
||||
}
|
||||
self.finish_pane_interaction();
|
||||
if let Some(owner) = self.diff_owner {
|
||||
self.with_pane(owner, |editor| {
|
||||
editor.release_diff_buffers();
|
||||
editor.diff = None;
|
||||
});
|
||||
self.parked_diff = None;
|
||||
self.diff_owner = None;
|
||||
self.outstanding_diff_request = None;
|
||||
}
|
||||
let id = self.tab_id;
|
||||
let index = self.tab_index();
|
||||
let next = self.tab_order[(index + 1) % self.tab_count()];
|
||||
self.select_tab(next);
|
||||
self.tabs.remove(&id);
|
||||
self.tab_order.remove(index);
|
||||
for pane in panes {
|
||||
self.panes.remove(&pane);
|
||||
}
|
||||
for buffer in buffers {
|
||||
self.forget_buffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
fn close_pane(&mut self, force: bool) {
|
||||
if self.pane_layout.len() == 1 {
|
||||
if !force && self.any_dirty() {
|
||||
if self.tab_count() > 1 {
|
||||
self.close_tab(force);
|
||||
} else if !force && self.any_dirty() {
|
||||
self.set_message("Unsaved changes; use :w or :pane.force_close");
|
||||
} else {
|
||||
self.quit = true;
|
||||
@@ -1702,12 +1992,12 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
let id = self.pane_id;
|
||||
let buffer_id = self.pane_owned_buffer(id);
|
||||
let shared = self.panes.iter().any(|(other, pane)| {
|
||||
*other != id
|
||||
&& (pane.buffer_id == buffer_id || self.pane_owned_buffer(*other) == buffer_id)
|
||||
});
|
||||
if !shared && !force && self.buffer(buffer_id).is_some_and(Buffer::dirty) {
|
||||
let buffers = self.closing_buffers(&[id]);
|
||||
if !force
|
||||
&& let Some(buffer_id) = buffers
|
||||
.iter()
|
||||
.find(|buffer| self.buffer(**buffer).is_some_and(Buffer::dirty))
|
||||
{
|
||||
self.set_message(format!(
|
||||
"Buffer {buffer_id} has unsaved changes; use :w or :pane.force_close"
|
||||
));
|
||||
@@ -1724,13 +2014,8 @@ impl Editor {
|
||||
let next = self.pane_layout.close(id).expect("another pane exists");
|
||||
self.select_pane(next);
|
||||
self.panes.remove(&id);
|
||||
if !shared {
|
||||
self.parked.remove(&buffer_id);
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.views.remove(&buffer_id);
|
||||
}
|
||||
self.jumps.retain(|jump| jump.buffer_id != buffer_id);
|
||||
self.jump_index = self.jump_index.min(self.jumps.len().saturating_sub(1));
|
||||
for buffer in buffers {
|
||||
self.forget_buffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1874,12 +2159,19 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff_result(&mut self, id: u64, result: Result<Outcome, String>) -> bool {
|
||||
if self.outstanding_diff_request != Some(id) {
|
||||
let owner = self
|
||||
.tabs
|
||||
.values()
|
||||
.find(|tab| tab.outstanding_diff_request == Some(id))
|
||||
.and_then(|tab| tab.diff_owner);
|
||||
return owner.is_some_and(|owner| {
|
||||
self.with_pane(owner, |editor| editor.apply_diff_result(id, result))
|
||||
});
|
||||
}
|
||||
if let Some(owner) = self.diff_owner.filter(|owner| *owner != self.pane_id) {
|
||||
return self.with_pane(owner, |editor| editor.apply_diff_result(id, result));
|
||||
}
|
||||
if self.outstanding_diff_request != Some(id) {
|
||||
return false;
|
||||
}
|
||||
self.outstanding_diff_request = None;
|
||||
let Some(diff) = &mut self.diff else {
|
||||
return false;
|
||||
@@ -2031,7 +2323,14 @@ impl Editor {
|
||||
self.activate(buffer);
|
||||
}
|
||||
self.parked.retain(|id, buffer| {
|
||||
buffer.diff_snapshot.is_none() || self.panes.values().any(|pane| pane.buffer_id == *id)
|
||||
buffer.diff_snapshot.is_none()
|
||||
|| self.panes.values().any(|pane| pane.buffer_id == *id)
|
||||
|| buffer.diff_snapshot.is_some_and(|role| {
|
||||
self.tabs.values().any(|tab| {
|
||||
tab.diff()
|
||||
.is_some_and(|diff| diff.comparison_id == Some(role.0))
|
||||
})
|
||||
})
|
||||
});
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.views
|
||||
@@ -2199,7 +2498,16 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_live_diff(&mut self) {
|
||||
for diff in self.diff.iter_mut().chain(self.parked_diff.iter_mut()) {
|
||||
for diff in self
|
||||
.diff
|
||||
.iter_mut()
|
||||
.chain(self.parked_diff.iter_mut())
|
||||
.chain(
|
||||
self.tabs
|
||||
.values_mut()
|
||||
.flat_map(|tab| tab.diff.iter_mut().chain(tab.parked_diff.iter_mut())),
|
||||
)
|
||||
{
|
||||
let buffer = diff.live_buffer.and_then(|id| {
|
||||
if id == self.active.id {
|
||||
Some(&self.active)
|
||||
@@ -2316,6 +2624,11 @@ impl Editor {
|
||||
| Action::PaneFocusRight
|
||||
| Action::PaneClose
|
||||
| Action::PaneForceClose
|
||||
| Action::TabNew
|
||||
| Action::TabPrevious
|
||||
| Action::TabNext
|
||||
| Action::TabClose
|
||||
| Action::TabForceClose
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -4331,6 +4644,11 @@ impl Editor {
|
||||
| Action::PaneFocusDown
|
||||
| Action::PaneFocusUp
|
||||
| Action::PaneFocusRight
|
||||
| Action::TabNew
|
||||
| Action::TabPrevious
|
||||
| Action::TabNext
|
||||
| Action::TabClose
|
||||
| Action::TabForceClose
|
||||
)
|
||||
{
|
||||
let action = *action;
|
||||
@@ -4588,6 +4906,11 @@ impl Editor {
|
||||
| Action::PaneFocusRight
|
||||
| Action::PaneClose
|
||||
| Action::PaneForceClose
|
||||
| Action::TabNew
|
||||
| Action::TabPrevious
|
||||
| Action::TabNext
|
||||
| Action::TabClose
|
||||
| Action::TabForceClose
|
||||
| Action::Save
|
||||
| Action::Quit
|
||||
| Action::ForceQuit
|
||||
@@ -5035,21 +5358,33 @@ impl Editor {
|
||||
));
|
||||
return;
|
||||
}
|
||||
if let Some(owner) = self.diff_owner
|
||||
&& (self.diff_return_buffer == Some(id)
|
||||
|| self
|
||||
.diff
|
||||
.as_ref()
|
||||
.or(self.parked_diff.as_ref())
|
||||
.is_some_and(|diff| diff.live_buffer == Some(id)))
|
||||
{
|
||||
let owners: Vec<_> = self
|
||||
.diff_owner
|
||||
.filter(|_| {
|
||||
self.diff_return_buffer == Some(id)
|
||||
|| self
|
||||
.workspace_diff()
|
||||
.is_some_and(|diff| diff.live_buffer == Some(id))
|
||||
})
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.tabs
|
||||
.values()
|
||||
.filter(|tab| {
|
||||
tab.diff_return_buffer == Some(id)
|
||||
|| tab.diff().is_some_and(|diff| diff.live_buffer == Some(id))
|
||||
})
|
||||
.filter_map(|tab| tab.diff_owner),
|
||||
)
|
||||
.collect();
|
||||
for owner in owners {
|
||||
self.with_pane(owner, |editor| {
|
||||
editor.release_diff_buffers();
|
||||
editor.diff = None;
|
||||
editor.parked_diff = None;
|
||||
editor.diff_owner = None;
|
||||
editor.outstanding_diff_request = None;
|
||||
});
|
||||
self.parked_diff = None;
|
||||
self.diff_owner = None;
|
||||
self.outstanding_diff_request = None;
|
||||
}
|
||||
let replacement_id = if id == self.active.id {
|
||||
let next = self.adjacent_buffer(true);
|
||||
@@ -5229,6 +5564,11 @@ impl Editor {
|
||||
| Action::PaneFocusRight
|
||||
| Action::PaneClose
|
||||
| Action::PaneForceClose
|
||||
| Action::TabNew
|
||||
| Action::TabPrevious
|
||||
| Action::TabNext
|
||||
| Action::TabClose
|
||||
| Action::TabForceClose
|
||||
) {
|
||||
if !args.is_empty() {
|
||||
self.set_message(format!(
|
||||
@@ -5246,6 +5586,11 @@ impl Editor {
|
||||
Action::PaneFocusRight => self.focus_pane(PaneDirection::Right),
|
||||
Action::PaneClose => self.close_pane(false),
|
||||
Action::PaneForceClose => self.close_pane(true),
|
||||
Action::TabNew => self.new_tab(),
|
||||
Action::TabPrevious => self.adjacent_tab(false),
|
||||
Action::TabNext => self.adjacent_tab(true),
|
||||
Action::TabClose => self.close_tab(false),
|
||||
Action::TabForceClose => self.close_tab(true),
|
||||
_ => unreachable!("pane actions handled above"),
|
||||
}
|
||||
return None;
|
||||
@@ -8308,6 +8653,354 @@ mod tests {
|
||||
assert_eq!(editor.buffer_id(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabs_preserve_panes_explorers_and_independent_views_of_shared_history() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
editor.project_root = directory.0.clone();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("abcdef".into())])
|
||||
.unwrap();
|
||||
editor.execute("pane.split_vertical");
|
||||
let source_pane = editor.pane_id();
|
||||
set_carets(&mut editor, &[(0, 2)], 0);
|
||||
editor.open_file_picker(FilePresentation::Docked);
|
||||
let layout = format!("{:?}", editor.pane_layout());
|
||||
editor.execute("tab.new");
|
||||
let new_pane = editor.pane_id();
|
||||
assert_ne!(source_pane, new_pane);
|
||||
assert_eq!(editor.tab_index(), 1);
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert_eq!(editor.buffer_id(), 1);
|
||||
assert_eq!(editor.buffers().count(), 1);
|
||||
assert_eq!(editor.pane_layout().len(), 1);
|
||||
assert!(editor.file_picker().is_none());
|
||||
set_carets(&mut editor, &[(0, 4)], 0);
|
||||
editor.handle(Input::CtrlAlt('h'));
|
||||
assert_eq!(editor.pane_id(), source_pane);
|
||||
assert_eq!(format!("{:?}", editor.pane_layout()), layout);
|
||||
assert_eq!(editor.cursor().column, 2);
|
||||
assert!(editor.file_picker().unwrap().focused);
|
||||
editor.handle(Input::CtrlAlt('l'));
|
||||
assert_eq!(editor.pane_id(), new_pane);
|
||||
assert_eq!(editor.cursor().column, 4);
|
||||
type_text(&mut editor, "iX");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::CtrlAlt('l'));
|
||||
assert_eq!(editor.tab_index(), 0);
|
||||
assert_eq!(editor.lines(), ["abcdXef"]);
|
||||
assert_eq!(editor.cursor().column, 2);
|
||||
editor.blur_file_picker();
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.lines(), ["abcdef"]);
|
||||
editor.handle(Input::CtrlAlt('h'));
|
||||
assert_eq!(editor.tab_index(), 1);
|
||||
assert_eq!(editor.lines(), ["abcdef"]);
|
||||
assert_eq!(editor.pane_buffer_id(source_pane), Some(1));
|
||||
assert_eq!(editor.tab_buffer(0).unwrap().id, 1);
|
||||
assert_eq!(editor.tab_buffer(1).unwrap().id, 1);
|
||||
assert!(editor.tab_buffer(2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_close_guards_final_dirty_owners_and_force_keeps_shared_documents() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
editor.apply(vec![Effect::Insert("shared".into())]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
editor.execute("tab.force_close");
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
assert!(editor.has_buffer(1));
|
||||
assert!(editor.dirty());
|
||||
editor.execute("tab.new");
|
||||
editor.execute(&format!(
|
||||
"edit {}",
|
||||
directory.path("exclusive.txt").display()
|
||||
));
|
||||
let exclusive = editor.buffer_id();
|
||||
editor
|
||||
.apply(vec![Effect::Insert("exclusive".into())])
|
||||
.unwrap();
|
||||
editor.execute("pane.split_horizontal");
|
||||
editor.execute("tab.close");
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert!(editor.message().contains("tab.force_close"));
|
||||
editor.execute("pane.close");
|
||||
assert_eq!(editor.pane_layout().len(), 1);
|
||||
editor.execute("pane.close");
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
editor.execute("pane.force_close");
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
assert!(editor.has_buffer(1));
|
||||
assert!(!editor.has_buffer(exclusive));
|
||||
assert_eq!(editor.lines(), ["shared"]);
|
||||
editor.execute("tab.close");
|
||||
assert!(!editor.should_quit());
|
||||
editor.execute("tab.force_close");
|
||||
assert!(editor.should_quit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_tab_checks_dirty_diff_return_buffer_even_when_live_buffer_is_shared() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("file.txt");
|
||||
fs::write(&path, "base").unwrap();
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
editor.execute(&format!("edit {}", directory.path("return.txt").display()));
|
||||
let return_buffer = editor.buffer_id();
|
||||
editor
|
||||
.apply(vec![Effect::Insert("dirty return".into())])
|
||||
.unwrap();
|
||||
working_diff(&mut editor, &directory.0, &["file.txt"]);
|
||||
editor.diff.as_mut().unwrap().focus = Focus::Before;
|
||||
editor.bind_diff_buffer();
|
||||
assert!(!editor.dirty());
|
||||
editor.execute("tab.close");
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert!(editor.message().contains("unsaved") || editor.message().contains("Unsaved"));
|
||||
editor.execute("tab.force_close");
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
assert!(editor.has_buffer(1));
|
||||
assert!(!editor.has_buffer(return_buffer));
|
||||
assert_eq!(editor.lines(), ["base"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_modifiers_aliases_catalog_and_palette_follow_pane_conventions() {
|
||||
for key in ["ctrl+alt+h", "CTRL+Option+H", "alt+ctrl+h", "option+ctrl+h"] {
|
||||
assert_eq!(parse_key(key).unwrap(), Input::CtrlAlt('h'));
|
||||
}
|
||||
for key in [
|
||||
"ctrl+shift+h",
|
||||
"ctrl+alt+shift+h",
|
||||
"alt+alt+h",
|
||||
"ctrl+alt+left",
|
||||
] {
|
||||
assert!(parse_key(key).is_err(), "{key}");
|
||||
}
|
||||
assert_eq!(display_key(&Input::CtrlAlt('h')), "Ctrl+Alt+h");
|
||||
let mut editor = editor();
|
||||
for command in [
|
||||
"tab.new",
|
||||
"tab.previous",
|
||||
"tab.next",
|
||||
"tab.close",
|
||||
"tab.force_close",
|
||||
] {
|
||||
assert!(core_command(command).is_some());
|
||||
assert!(Action::named(Mode::Normal, &format!("core.{command}")).is_some());
|
||||
assert!(Action::named(Mode::Insert, &format!("core.{command}")).is_none());
|
||||
}
|
||||
editor.execute("tab.new extra");
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
type_text(&mut editor, "core.tab.new");
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::CtrlAlt('h'));
|
||||
assert_eq!(editor.tab_index(), 1);
|
||||
editor.handle(Input::Escape);
|
||||
editor.bind("normal", "option+ctrl+h", None).unwrap();
|
||||
editor.handle(Input::CtrlAlt('h'));
|
||||
assert_eq!(editor.tab_index(), 1);
|
||||
editor
|
||||
.bind("normal", "ctrl+alt+h", Some("core.tab.previous".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::CtrlAlt('h'));
|
||||
assert_eq!(editor.tab_index(), 0);
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
type_text(&mut editor, "core.tab.force_close");
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.command_line(), "tab.force_close ");
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_tab_diff_results_restore_visible_insert_interaction_and_route_request_ids() {
|
||||
use crate::diff::{
|
||||
Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision,
|
||||
};
|
||||
let mut editor = editor();
|
||||
editor.execute("diff");
|
||||
let first_tab = editor.tab_id();
|
||||
let (first_request, _) = editor.take_diff_request().unwrap();
|
||||
editor.execute("pane.split_vertical");
|
||||
let hidden_focused_pane = editor.pane_id();
|
||||
editor.execute("tab.new");
|
||||
editor.execute("diff");
|
||||
let second_tab = editor.tab_id();
|
||||
let (second_request, _) = editor.take_diff_request().unwrap();
|
||||
assert!(second_request > first_request);
|
||||
editor.execute("tab.new");
|
||||
let visible_tab = editor.tab_id();
|
||||
let visible_pane = editor.pane_id();
|
||||
type_text(&mut editor, "iX");
|
||||
let cursor = editor.cursor();
|
||||
editor.set_message("visible");
|
||||
let catalog = || {
|
||||
Outcome::Catalog(Catalog {
|
||||
kind: RepoKind::Git,
|
||||
entries: [
|
||||
RevisionTarget::Commit("base".into()),
|
||||
RevisionTarget::Working,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|target| Revision {
|
||||
target,
|
||||
label: "Revision".into(),
|
||||
description: String::new(),
|
||||
category: Category::Commits,
|
||||
})
|
||||
.collect(),
|
||||
notice: None,
|
||||
})
|
||||
};
|
||||
assert!(editor.apply_diff_result(second_request, Ok(catalog())));
|
||||
assert!(editor.apply_diff_result(first_request, Ok(catalog())));
|
||||
assert!(!editor.apply_diff_result(first_request, Ok(catalog())));
|
||||
assert_eq!(editor.tab_id(), visible_tab);
|
||||
assert_eq!(editor.pane_id(), visible_pane);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.message(), "visible");
|
||||
assert_eq!(editor.lines(), ["X"]);
|
||||
assert!(editor.diff().is_none());
|
||||
assert!(!editor.tab_diff(first_tab).unwrap().loading);
|
||||
assert!(!editor.tab_diff(second_tab).unwrap().loading);
|
||||
let owner = editor.tabs[&first_tab].diff_owner.unwrap();
|
||||
let (comparison_request, _) = editor.with_pane(owner, |editor| {
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Enter);
|
||||
editor.take_diff_request().unwrap()
|
||||
});
|
||||
let before = vec!["base".to_owned()];
|
||||
let after = vec!["changed".to_owned()];
|
||||
let hunks = crate::diff::line_hunks(&before, false, &after, false);
|
||||
assert!(editor.apply_diff_result(
|
||||
comparison_request,
|
||||
Ok(Outcome::Comparison(Comparison {
|
||||
root: editor.project_root().to_owned(),
|
||||
before: "base".into(),
|
||||
after: "working".into(),
|
||||
editable_paths: Default::default(),
|
||||
files: vec![ChangedFile {
|
||||
loaded: true,
|
||||
path: "file.txt".into(),
|
||||
old_path: None,
|
||||
status: FileStatus::Modified,
|
||||
before,
|
||||
after,
|
||||
before_eof: false,
|
||||
after_eof: false,
|
||||
hunks,
|
||||
notice: None,
|
||||
}],
|
||||
}))
|
||||
));
|
||||
assert_eq!(editor.tab_id(), visible_tab);
|
||||
assert_eq!(editor.pane_id(), visible_pane);
|
||||
assert_eq!(editor.buffer_id(), 1);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.message(), "visible");
|
||||
assert_ne!(editor.pane_buffer_id(owner), Some(1));
|
||||
assert_eq!(editor.tabs[&first_tab].pane_id, hidden_focused_pane);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.lines(), [""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_snapshots_remain_readonly_and_other_comparisons_survive_release() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["base".into()],
|
||||
vec!["first".into()],
|
||||
);
|
||||
let first_tab = editor.tab_id();
|
||||
let first_snapshot = editor.buffer_id();
|
||||
let first_comparison = editor.diff().unwrap().comparison_id;
|
||||
editor.execute("tab.new");
|
||||
assert_eq!(editor.buffer_id(), first_snapshot);
|
||||
assert!(editor.diff().is_none());
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![Effect::Insert("rejected".into())])
|
||||
.is_err()
|
||||
);
|
||||
editor.execute("buffer 1");
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["base".into()],
|
||||
vec!["second".into()],
|
||||
);
|
||||
let second_snapshot = editor.buffer_id();
|
||||
assert_ne!(editor.diff().unwrap().comparison_id, first_comparison);
|
||||
editor.execute("tab.new");
|
||||
assert_eq!(editor.buffer_id(), second_snapshot);
|
||||
editor.execute("tab.previous");
|
||||
editor.execute("diff.close");
|
||||
assert!(editor.tab_diff(first_tab).is_some());
|
||||
assert!(editor.has_buffer(first_snapshot));
|
||||
assert!(editor.has_buffer(second_snapshot));
|
||||
editor.execute("tab.next");
|
||||
assert_eq!(editor.buffer_id(), second_snapshot);
|
||||
assert!(editor.mutation_blocked());
|
||||
editor.execute("tab.next");
|
||||
assert_eq!(editor.buffer_id(), first_snapshot);
|
||||
assert_eq!(editor.diff().unwrap().comparison_id, first_comparison);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_tab_comparisons_refresh_shared_edits_and_deletion_closes_all_owners() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("file.txt");
|
||||
fs::write(&path, "base").unwrap();
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
working_diff(&mut editor, &directory.0, &["file.txt"]);
|
||||
editor.diff.as_mut().unwrap().focus = Focus::After;
|
||||
editor.bind_diff_buffer();
|
||||
let first_tab = editor.tab_id();
|
||||
editor.execute("tab.new");
|
||||
working_diff(&mut editor, &directory.0, &["file.txt"]);
|
||||
editor.diff.as_mut().unwrap().focus = Focus::After;
|
||||
editor.bind_diff_buffer();
|
||||
let second_tab = editor.tab_id();
|
||||
editor.execute("tab.new");
|
||||
editor
|
||||
.apply(vec![Effect::Insert("changed".into())])
|
||||
.unwrap();
|
||||
let revision = editor.text_revision();
|
||||
assert_eq!(
|
||||
editor.tab_diff(first_tab).unwrap().live_revision,
|
||||
Some(revision)
|
||||
);
|
||||
assert_eq!(
|
||||
editor.tab_diff(second_tab).unwrap().live_revision,
|
||||
Some(revision)
|
||||
);
|
||||
editor.execute("tab.close");
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert!(editor.has_buffer(1));
|
||||
editor.execute("bdelete! 1");
|
||||
assert!(!editor.has_buffer(1));
|
||||
assert!(editor.tab_diff(first_tab).is_none());
|
||||
assert!(editor.tab_diff(second_tab).is_none());
|
||||
editor.execute("tab.next");
|
||||
assert!(editor.diff().is_none());
|
||||
assert!(editor.has_buffer(editor.buffer_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_views_survive_buffer_switches_and_deletion_replaces_every_reference() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user