feat: add panes and shared-buffer splits
This commit is contained in:
+1529
-445
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ pub mod files;
|
||||
pub mod lsp;
|
||||
mod lsp_config;
|
||||
mod markdown;
|
||||
pub mod panes;
|
||||
pub mod plugin;
|
||||
pub mod repository;
|
||||
pub mod ruby;
|
||||
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
//! Frontend-independent binary pane layout and spatial focus navigation.
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SplitAxis {
|
||||
/// Above/below.
|
||||
Horizontal,
|
||||
/// Left/right.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PaneDirection {
|
||||
Left,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct PaneRect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl PaneRect {
|
||||
fn is_empty(self) -> bool {
|
||||
self.width == 0 || self.height == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Partition a rectangle into balanced halves, with the odd cell in the second.
|
||||
pub fn split_rect(bounds: PaneRect, axis: SplitAxis) -> [PaneRect; 2] {
|
||||
let mut first = bounds;
|
||||
let mut second = bounds;
|
||||
match axis {
|
||||
SplitAxis::Horizontal => {
|
||||
first.height = bounds.height / 2;
|
||||
second.y = bounds.y.saturating_add(first.height);
|
||||
second.height -= first.height;
|
||||
}
|
||||
SplitAxis::Vertical => {
|
||||
first.width = bounds.width / 2;
|
||||
second.x = bounds.x.saturating_add(first.width);
|
||||
second.width -= first.width;
|
||||
}
|
||||
}
|
||||
[first, second]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Node {
|
||||
Leaf(usize),
|
||||
Split {
|
||||
axis: SplitAxis,
|
||||
children: Box<[Node; 2]>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn contains(&self, id: usize) -> bool {
|
||||
match self {
|
||||
Self::Leaf(leaf) => *leaf == id,
|
||||
Self::Split { children, .. } => children.iter().any(|child| child.contains(id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_id(&self) -> usize {
|
||||
match self {
|
||||
Self::Leaf(id) => *id,
|
||||
Self::Split { children, .. } => children[0].first_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool {
|
||||
match self {
|
||||
Self::Leaf(leaf) if *leaf == id => {
|
||||
*self = Self::Split {
|
||||
axis,
|
||||
children: Box::new([Self::Leaf(id), Self::Leaf(new_id)]),
|
||||
};
|
||||
true
|
||||
}
|
||||
Self::Leaf(_) => false,
|
||||
Self::Split { children, .. } => children
|
||||
.iter_mut()
|
||||
.any(|child| child.split(id, new_id, axis)),
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Option<usize> {
|
||||
let Self::Split { children, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
for index in 0..2 {
|
||||
if matches!(children[index], Self::Leaf(leaf) if leaf == id) {
|
||||
let sibling = 1 - index;
|
||||
let focus = children[sibling].first_id();
|
||||
*self = std::mem::replace(&mut children[sibling], Self::Leaf(id));
|
||||
return Some(focus);
|
||||
}
|
||||
}
|
||||
children.iter_mut().find_map(|child| child.close(id))
|
||||
}
|
||||
|
||||
fn visit_rects(&self, bounds: PaneRect, visitor: &mut impl FnMut(usize, PaneRect)) {
|
||||
if bounds.is_empty() {
|
||||
return;
|
||||
}
|
||||
match self {
|
||||
Self::Leaf(id) => visitor(*id, bounds),
|
||||
Self::Split { axis, children } => {
|
||||
let halves = split_rect(bounds, *axis);
|
||||
children[0].visit_rects(halves[0], visitor);
|
||||
children[1].visit_rects(halves[1], visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PaneTree {
|
||||
root: Node,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl PaneTree {
|
||||
pub fn new(id: usize) -> Self {
|
||||
Self {
|
||||
root: Node::Leaf(id),
|
||||
len: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep `id` first and insert `new_id` second. IDs must be unique.
|
||||
pub fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool {
|
||||
if self.contains(new_id) || !self.root.split(id, new_id, axis) {
|
||||
return false;
|
||||
}
|
||||
self.len += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Close a leaf and expand its sibling; return the sibling's first leaf.
|
||||
/// Unknown IDs and closing the last leaf leave the tree unchanged.
|
||||
pub fn close(&mut self, id: usize) -> Option<usize> {
|
||||
let focus = self.root.close(id)?;
|
||||
self.len -= 1;
|
||||
Some(focus)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: usize) -> bool {
|
||||
self.root.contains(id)
|
||||
}
|
||||
|
||||
/// Visit nonempty leaves in tree order, without allocating.
|
||||
pub fn visit_rects(&self, bounds: PaneRect, mut visitor: impl FnMut(usize, PaneRect)) {
|
||||
self.root.visit_rects(bounds, &mut visitor);
|
||||
}
|
||||
|
||||
pub fn neighbor(&self, id: usize, direction: PaneDirection, bounds: PaneRect) -> Option<usize> {
|
||||
let mut current = None;
|
||||
self.visit_rects(bounds, |leaf, rect| {
|
||||
if leaf == id {
|
||||
current = Some(rect);
|
||||
}
|
||||
});
|
||||
let current = current?;
|
||||
let mut best = None;
|
||||
self.visit_rects(bounds, |leaf, rect| {
|
||||
if leaf != id {
|
||||
consider_neighbor(current, direction, leaf, rect, &mut best);
|
||||
}
|
||||
});
|
||||
best.map(|(_, leaf)| leaf)
|
||||
}
|
||||
}
|
||||
|
||||
// Score: prefer perpendicular overlap, then the nearest forward edge,
|
||||
// perpendicular gap, center alignment, and finally the stable ID.
|
||||
type NeighborScore = (bool, u32, u32, u32, usize);
|
||||
|
||||
fn neighbor_score(
|
||||
current: PaneRect,
|
||||
direction: PaneDirection,
|
||||
id: usize,
|
||||
candidate: PaneRect,
|
||||
) -> Option<NeighborScore> {
|
||||
if current.is_empty() || candidate.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let edges = |rect: PaneRect| {
|
||||
let x = u32::from(rect.x);
|
||||
let y = u32::from(rect.y);
|
||||
(x, y, x + u32::from(rect.width), y + u32::from(rect.height))
|
||||
};
|
||||
let (x, y, right, bottom) = edges(current);
|
||||
let (cx, cy, cright, cbottom) = edges(candidate);
|
||||
let (distance, start, end, cstart, cend) = match direction {
|
||||
PaneDirection::Left if cright <= x => (x - cright, y, bottom, cy, cbottom),
|
||||
PaneDirection::Right if cx >= right => (cx - right, y, bottom, cy, cbottom),
|
||||
PaneDirection::Up if cbottom <= y => (y - cbottom, x, right, cx, cright),
|
||||
PaneDirection::Down if cy >= bottom => (cy - bottom, x, right, cx, cright),
|
||||
_ => return None,
|
||||
};
|
||||
let overlap = start.max(cstart) < end.min(cend);
|
||||
let gap = start.max(cstart).saturating_sub(end.min(cend));
|
||||
let alignment = (start + end).abs_diff(cstart + cend);
|
||||
Some((!overlap, distance, gap, alignment, id))
|
||||
}
|
||||
|
||||
fn consider_neighbor(
|
||||
current: PaneRect,
|
||||
direction: PaneDirection,
|
||||
id: usize,
|
||||
candidate: PaneRect,
|
||||
best: &mut Option<(NeighborScore, usize)>,
|
||||
) {
|
||||
if let Some(score) = neighbor_score(current, direction, id, candidate)
|
||||
&& best.as_ref().is_none_or(|(previous, _)| score < *previous)
|
||||
{
|
||||
*best = Some((score, id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a forward rectangle, favoring overlap and the closest edge.
|
||||
/// Empty rectangles are ignored; ties are independent of iterator order.
|
||||
pub fn directional_neighbor(
|
||||
current: PaneRect,
|
||||
direction: PaneDirection,
|
||||
candidates: impl Iterator<Item = (usize, PaneRect)>,
|
||||
) -> Option<usize> {
|
||||
let mut best = None;
|
||||
for (id, candidate) in candidates {
|
||||
consider_neighbor(current, direction, id, candidate, &mut best);
|
||||
}
|
||||
best.map(|(_, id)| id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rect(x: u16, y: u16, width: u16, height: u16) -> PaneRect {
|
||||
PaneRect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn leaves(tree: &PaneTree, bounds: PaneRect) -> Vec<(usize, PaneRect)> {
|
||||
let mut leaves = Vec::new();
|
||||
tree.visit_rects(bounds, |id, rect| leaves.push((id, rect)));
|
||||
leaves
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_layout_partitions_bounds_and_preserves_ids() {
|
||||
let bounds = rect(3, 5, 11, 9);
|
||||
let mut tree = PaneTree::new(10);
|
||||
assert!(tree.split(10, 20, SplitAxis::Vertical));
|
||||
assert!(tree.split(20, 30, SplitAxis::Horizontal));
|
||||
assert!(!tree.split(99, 40, SplitAxis::Horizontal));
|
||||
assert!(!tree.split(10, 30, SplitAxis::Horizontal));
|
||||
assert!(!tree.split(10, 10, SplitAxis::Horizontal));
|
||||
assert_eq!(tree.len(), 3);
|
||||
let panes = leaves(&tree, bounds);
|
||||
assert_eq!(
|
||||
panes,
|
||||
vec![
|
||||
(10, rect(3, 5, 5, 9)),
|
||||
(20, rect(8, 5, 6, 4)),
|
||||
(30, rect(8, 9, 6, 5))
|
||||
]
|
||||
);
|
||||
let mut cells = std::collections::HashSet::new();
|
||||
for (_, pane) in panes {
|
||||
for y in pane.y..pane.y + pane.height {
|
||||
for x in pane.x..pane.x + pane.width {
|
||||
assert!(cells.insert((x, y)), "overlapping pane cell");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
cells.len(),
|
||||
usize::from(bounds.width) * usize::from(bounds.height)
|
||||
);
|
||||
assert!(leaves(&tree, rect(0, 0, 0, 10)).is_empty());
|
||||
assert!(leaves(&tree, rect(0, 0, 10, 0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_expands_nested_sibling_and_refuses_last() {
|
||||
let bounds = rect(0, 0, 20, 12);
|
||||
let mut tree = PaneTree::new(1);
|
||||
tree.split(1, 2, SplitAxis::Vertical);
|
||||
tree.split(2, 3, SplitAxis::Horizontal);
|
||||
assert_eq!(tree.close(1), Some(2));
|
||||
assert_eq!(
|
||||
leaves(&tree, bounds),
|
||||
vec![(2, rect(0, 0, 20, 6)), (3, rect(0, 6, 20, 6))]
|
||||
);
|
||||
assert_eq!(tree.close(99), None);
|
||||
assert_eq!(tree.close(2), Some(3));
|
||||
assert_eq!(leaves(&tree, bounds), vec![(3, bounds)]);
|
||||
assert_eq!(tree.close(3), None);
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert!(!tree.contains(1));
|
||||
assert!(tree.contains(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directional_focus_handles_unequal_nested_regions() {
|
||||
let bounds = rect(0, 0, 20, 12);
|
||||
let mut tree = PaneTree::new(1);
|
||||
tree.split(1, 2, SplitAxis::Vertical);
|
||||
tree.split(2, 3, SplitAxis::Horizontal);
|
||||
tree.split(3, 4, SplitAxis::Horizontal);
|
||||
assert_eq!(tree.neighbor(1, PaneDirection::Right, bounds), Some(3));
|
||||
assert_eq!(tree.neighbor(2, PaneDirection::Down, bounds), Some(3));
|
||||
assert_eq!(tree.neighbor(3, PaneDirection::Down, bounds), Some(4));
|
||||
assert_eq!(tree.neighbor(4, PaneDirection::Up, bounds), Some(3));
|
||||
assert_eq!(tree.neighbor(4, PaneDirection::Left, bounds), Some(1));
|
||||
assert_eq!(tree.neighbor(1, PaneDirection::Left, bounds), None);
|
||||
assert_eq!(tree.neighbor(99, PaneDirection::Right, bounds), None);
|
||||
assert_eq!(
|
||||
tree.neighbor(1, PaneDirection::Right, rect(0, 0, 0, 0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_prefers_overlap_then_edge_and_breaks_ties_by_id() {
|
||||
let current = rect(10, 10, 10, 10);
|
||||
let candidates = [
|
||||
(1, rect(20, 0, 5, 5)),
|
||||
(2, rect(25, 10, 5, 10)),
|
||||
(4, rect(22, 10, 5, 10)),
|
||||
(3, rect(22, 10, 5, 10)),
|
||||
(0, rect(20, 10, 0, 10)),
|
||||
];
|
||||
assert_eq!(
|
||||
directional_neighbor(current, PaneDirection::Right, candidates.into_iter()),
|
||||
Some(3)
|
||||
);
|
||||
assert_eq!(
|
||||
directional_neighbor(current, PaneDirection::Right, candidates.into_iter().rev()),
|
||||
Some(3)
|
||||
);
|
||||
assert_eq!(
|
||||
directional_neighbor(
|
||||
current,
|
||||
PaneDirection::Up,
|
||||
[(7, rect(10, 0, 10, 5))].into_iter()
|
||||
),
|
||||
Some(7)
|
||||
);
|
||||
assert_eq!(
|
||||
directional_neighbor(current, PaneDirection::Left, candidates.into_iter()),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
+75
-10
@@ -145,6 +145,7 @@ struct TrackedDocument {
|
||||
#[derive(Clone, Copy)]
|
||||
struct InvocationSource {
|
||||
buffer_id: usize,
|
||||
pane_id: usize,
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
@@ -270,6 +271,11 @@ impl EditorSession {
|
||||
&self.editor
|
||||
}
|
||||
|
||||
/// Rendering-only mutable access; document synchronization stays in session dispatch.
|
||||
pub(crate) fn editor_mut(&mut self) -> &mut Editor {
|
||||
&mut self.editor
|
||||
}
|
||||
|
||||
pub(crate) fn set_diff_viewport(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
@@ -313,11 +319,14 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
fn start_diff_request(&mut self) {
|
||||
if self.editor.diff().is_none() {
|
||||
if self.editor.workspace_diff().is_none() {
|
||||
self.diff_worker = None;
|
||||
return;
|
||||
}
|
||||
if self.editor.diff().is_some_and(|diff| !diff.loading)
|
||||
if self
|
||||
.editor
|
||||
.workspace_diff()
|
||||
.is_some_and(|diff| !diff.loading)
|
||||
&& let Some(worker) = &mut self.diff_worker
|
||||
&& let Some(id) = worker.pending.take()
|
||||
{
|
||||
@@ -328,13 +337,7 @@ impl EditorSession {
|
||||
return;
|
||||
};
|
||||
if self.diff_worker.is_none() {
|
||||
let root = self
|
||||
.editor
|
||||
.path()
|
||||
.and_then(Path::parent)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| self.editor.project_root())
|
||||
.to_path_buf();
|
||||
let root = self.editor.diff_root();
|
||||
match DiffWorker::start(root) {
|
||||
Ok(worker) => self.diff_worker = Some(worker),
|
||||
Err(error) => {
|
||||
@@ -396,6 +399,7 @@ impl EditorSession {
|
||||
let context = self.editor.context();
|
||||
let source = InvocationSource {
|
||||
buffer_id: context.buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision: context.revision,
|
||||
};
|
||||
self.requests.entry("core".into()).or_default().latest = Some((id, source));
|
||||
@@ -431,6 +435,7 @@ impl EditorSession {
|
||||
.map(|id| {
|
||||
let source = InvocationSource {
|
||||
buffer_id: context.buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision: context.revision,
|
||||
};
|
||||
requests.pending.insert(id, source);
|
||||
@@ -484,6 +489,7 @@ impl EditorSession {
|
||||
if let Some(requests) = self.requests.get_mut("core")
|
||||
&& requests.latest.is_some_and(|(_, source)| {
|
||||
source.buffer_id != self.editor.buffer_id()
|
||||
|| source.pane_id != self.editor.pane_id()
|
||||
|| source.revision != self.editor.text_revision()
|
||||
|| self.lsp_source_cursor != Some(self.editor.cursor())
|
||||
|| self.editor.mode() != self.lsp_source_mode
|
||||
@@ -618,6 +624,7 @@ impl EditorSession {
|
||||
.is_some_and(|(id, latest)| {
|
||||
request_id == Some(id)
|
||||
&& latest.buffer_id == source.buffer_id
|
||||
&& latest.pane_id == self.editor.pane_id()
|
||||
&& latest.revision == source.revision
|
||||
})
|
||||
}
|
||||
@@ -674,6 +681,7 @@ impl EditorSession {
|
||||
}
|
||||
let source = InvocationSource {
|
||||
buffer_id: *buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision: *revision,
|
||||
};
|
||||
if self.completion_is_current(update.request_id, source)
|
||||
@@ -720,6 +728,7 @@ impl EditorSession {
|
||||
} => {
|
||||
let source = InvocationSource {
|
||||
buffer_id: *buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision: *revision,
|
||||
};
|
||||
if self.request_is_current(owner, update.request_id, source)
|
||||
@@ -738,6 +747,7 @@ impl EditorSession {
|
||||
} => {
|
||||
let source = InvocationSource {
|
||||
buffer_id: *buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision: *revision,
|
||||
};
|
||||
if self.hover_is_current(owner, update.request_id, source) {
|
||||
@@ -786,6 +796,7 @@ impl EditorSession {
|
||||
} => {
|
||||
let source = InvocationSource {
|
||||
buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision,
|
||||
};
|
||||
if self.request_is_current(owner, update.request_id, source) {
|
||||
@@ -805,6 +816,7 @@ impl EditorSession {
|
||||
} => {
|
||||
let source = InvocationSource {
|
||||
buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision,
|
||||
};
|
||||
if self.hover_is_current(owner, update.request_id, source) {
|
||||
@@ -823,6 +835,7 @@ impl EditorSession {
|
||||
} => {
|
||||
let source = InvocationSource {
|
||||
buffer_id,
|
||||
pane_id: self.editor.pane_id(),
|
||||
revision,
|
||||
};
|
||||
if self.completion_is_current(update.request_id, source) {
|
||||
@@ -857,7 +870,10 @@ impl EditorSession {
|
||||
let Some(source) = source else {
|
||||
return Err("plugin response has an unknown invocation ID".into());
|
||||
};
|
||||
if !self.source_is_current(source) || self.editor.buffer_id() != source.buffer_id {
|
||||
if !self.source_is_current(source)
|
||||
|| self.editor.buffer_id() != source.buffer_id
|
||||
|| self.editor.pane_id() != source.pane_id
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if effects
|
||||
@@ -948,6 +964,53 @@ mod tests {
|
||||
|
||||
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn render_swaps_preserve_session_sources_and_pending_effects_do_not_cross_panes() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file("buffer.txt", "source");
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
let source = InvocationSource {
|
||||
pane_id: session.editor.pane_id(),
|
||||
buffer_id: session.editor.buffer_id(),
|
||||
revision: session.editor.text_revision(),
|
||||
};
|
||||
session
|
||||
.requests
|
||||
.entry("fixture".into())
|
||||
.or_default()
|
||||
.pending
|
||||
.insert(1, source);
|
||||
session.requests.entry("fixture".into()).or_default().latest = Some((1, source));
|
||||
session.handle(Input::Char('|'));
|
||||
assert_eq!(session.editor.buffer_id(), source.buffer_id);
|
||||
assert_eq!(session.editor.cursor(), Cursor::default());
|
||||
assert!(!session.request_is_current("fixture", Some(1), source));
|
||||
assert!(
|
||||
!session
|
||||
.apply_response("fixture", 1, vec![Effect::Insert("wrong pane".into())])
|
||||
.unwrap()
|
||||
);
|
||||
session.requests.entry("core".into()).or_default().latest = Some((
|
||||
2,
|
||||
InvocationSource {
|
||||
pane_id: session.editor.pane_id(),
|
||||
..source
|
||||
},
|
||||
));
|
||||
session.lsp_source_cursor = Some(session.editor.cursor());
|
||||
session.lsp_source_mode = session.editor.mode();
|
||||
session.editor_mut().with_render_pane(1, |pane| {
|
||||
assert_eq!(pane.pane_id(), 1);
|
||||
assert_eq!(pane.lines(), ["source"]);
|
||||
});
|
||||
session.synchronize_documents();
|
||||
assert!(session.requests["core"].latest.is_some());
|
||||
assert_eq!(session.editor.pane_id(), 2);
|
||||
session.handle(Input::Alt('h'));
|
||||
assert!(session.requests["core"].latest.is_none());
|
||||
assert_eq!(session.editor.lines(), ["source"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_semantic_batches_are_atomic_and_malformed_updates_clear_hidden_overlays() {
|
||||
use crate::plugin::{SemanticToken, SemanticTokenKind};
|
||||
@@ -3412,6 +3475,7 @@ end
|
||||
session.editor.handle(Input::BackTab);
|
||||
let source = InvocationSource {
|
||||
buffer_id: session.editor.buffer_id(),
|
||||
pane_id: session.editor.pane_id(),
|
||||
revision: session.editor.text_revision(),
|
||||
};
|
||||
session
|
||||
@@ -3447,6 +3511,7 @@ end
|
||||
session.editor.scroll_diff(Focus::Before, false);
|
||||
let readonly = InvocationSource {
|
||||
buffer_id: session.editor.buffer_id(),
|
||||
pane_id: session.editor.pane_id(),
|
||||
revision: session.editor.text_revision(),
|
||||
};
|
||||
session
|
||||
|
||||
+975
-315
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user