mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
#799 landed first and rebuilt the overlay as one virtualised `gpui::list` of flat rows, deleting the card, the hunk nesting and the three row builders this branch had hung the drag on. The selection is re-applied onto that model rather than rebuilt beside it. **Where a row is.** `DiffRow::Split`/`Unified` now carry a `RowAt` — the file's path and the `RowId { hunk, row }` the range is keyed on. The list index is deliberately *not* that key: `resync_list` splices only the rows between a shared prefix and suffix, so collapsing a file above the selection moves every index below it, and a range keyed on those would go on naming the same slots while the code under them changed. `RowAt::path` is a `SharedString` so one rides on every line of a patch for a refcount bump. **Where the selection is read.** `diff_rows_list` clones it into a `Drag` once a frame and the list's item builder reads that. It stays out of `RowsKey` on purpose: a selection changes what a row looks like, not which rows there are, so a step of a drag notifies, re-renders the rows on screen, and never re-flattens the patch — `gpui::list` calls its item builder for every visible row on each layout pass, so the highlight is current without the key knowing anything about it. **Where the menu is.** `diff_file_card` was the old host and is gone. Copy Selected Lines now hangs on the selected rows themselves. The overlay root was the other candidate and is not usable: the header already carries `document_header_menu`, and gpui-component's context menu opens on a bubbling right-press without stopping propagation, so two of them over one click would open two popups. The padding-cell fix from 87dc26f0..f7fd1d43 is carried across: a blank half of a split row takes the same press and move listeners as any other cell, and the selection colour when the range covers it. Also drops `HunkRows::len`/`is_empty`, which only the deleted card body called. Tests: the drag suite moves to `selection_gpui_tests` and drives the same cases against the new state, including the upward drag; two new `diff_list` tests pin the coordinates a row carries — that they name the patch rather than the list, and that they survive a collapse above them.
576 lines
19 KiB
Rust
576 lines
19 KiB
Rust
//! Flattening a diff snapshot into the one-dimensional list of rows the
|
|
//! overlay scrolls.
|
|
//!
|
|
//! The overlay used to build its whole patch as a nested element tree —
|
|
//! a card per file, a hunk header and one element per line inside it, every
|
|
//! one of them constructed on every frame. A `20_000` line budget is around
|
|
//! `120_000` elements to lay out, and gpui notifies the view on each scroll
|
|
//! wheel event, so a diff of any size rebuilt the entire tree tens of times a
|
|
//! second.
|
|
//!
|
|
//! [`gpui::list`] only builds the rows it can see, but it is one-dimensional:
|
|
//! it takes an index, not a tree. So the tree is flattened here, once per
|
|
//! change to what is on screen.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use gpui::SharedString;
|
|
|
|
use crate::core::config::DiffViewMode;
|
|
use crate::terminal::git_diff::{
|
|
AUTO_COLLAPSE_LINES, DiffSnapshot, FileDiff, FileStatus, MAX_RENDERED_FILES, Truncation,
|
|
};
|
|
use crate::ui::diff_rows::{RowId, SplitRow, UnifiedRow, split_hunk, unified_rows};
|
|
|
|
/// Everything the file header row draws, lifted out of its [`FileDiff`].
|
|
///
|
|
/// A copy rather than a borrow: the row list outlives the frame that built it,
|
|
/// and these are a handful of small fields against a file's whole patch.
|
|
#[derive(PartialEq, Eq)]
|
|
pub(crate) struct FileHead {
|
|
/// Position in `snap.files`, for the element id and nothing else.
|
|
pub(crate) index: usize,
|
|
pub(crate) path: String,
|
|
/// `old → new` for a rename, the path itself otherwise.
|
|
pub(crate) shown_path: String,
|
|
pub(crate) status: FileStatus,
|
|
pub(crate) added: u32,
|
|
pub(crate) removed: u32,
|
|
pub(crate) binary: bool,
|
|
pub(crate) expandable: bool,
|
|
pub(crate) expanded: bool,
|
|
}
|
|
|
|
/// Where a drawn line sits in the patch: which file's rows it belongs to, and
|
|
/// its place among them.
|
|
///
|
|
/// Carried on the row rather than read off its position in the list. The list
|
|
/// is spliced — collapsing a file above this one moves every index below it —
|
|
/// so a range keyed on list positions would go on naming the same slots while
|
|
/// the code under them changed.
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
pub(crate) struct RowAt {
|
|
/// One of these rides on every line of a patch, so the path is shared
|
|
/// rather than copied per row.
|
|
pub(crate) path: SharedString,
|
|
pub(crate) id: RowId,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
pub(crate) enum DiffRow {
|
|
/// The space that used to be the `gap_3` of a flex column.
|
|
Gap,
|
|
/// The banner above an auto-collapsed tree. Its text is derived from the
|
|
/// snapshot at render time, so nothing is carried here.
|
|
Oversized,
|
|
FileHeader(FileHead),
|
|
HunkHeader {
|
|
text: String,
|
|
/// The first hunk of a file follows its header and needs no rule
|
|
/// above it; every later one is separating itself from the lines of
|
|
/// the hunk before.
|
|
leads: bool,
|
|
},
|
|
Split {
|
|
row: SplitRow,
|
|
at: RowAt,
|
|
},
|
|
Unified {
|
|
row: UnifiedRow,
|
|
at: RowAt,
|
|
},
|
|
Truncated(Truncation),
|
|
MoreFiles {
|
|
rest: usize,
|
|
},
|
|
UntrackedHeader {
|
|
total: usize,
|
|
},
|
|
Untracked {
|
|
index: usize,
|
|
path: String,
|
|
},
|
|
MoreUntracked {
|
|
rest: usize,
|
|
},
|
|
}
|
|
|
|
/// The rows of the whole overlay, in the order they scroll past.
|
|
pub(crate) fn build_rows(
|
|
snap: &DiffSnapshot,
|
|
expanded: &HashMap<String, bool>,
|
|
focused: Option<usize>,
|
|
mode: DiffViewMode,
|
|
oversized: bool,
|
|
) -> Vec<DiffRow> {
|
|
let mut out: Vec<DiffRow> = Vec::new();
|
|
// Stands in for the gap between the groups this list used to be a flex
|
|
// column of: gpui's list stacks its items with nothing between them.
|
|
let gap = |out: &mut Vec<DiffRow>| {
|
|
if !out.is_empty() {
|
|
out.push(DiffRow::Gap);
|
|
}
|
|
};
|
|
|
|
if oversized {
|
|
out.push(DiffRow::Oversized);
|
|
}
|
|
|
|
let shown = snap.files.len().min(MAX_RENDERED_FILES);
|
|
for (idx, file) in snap.files.iter().enumerate() {
|
|
if focused.is_some_and(|f| f != idx) {
|
|
continue;
|
|
}
|
|
if focused.is_none() && idx >= shown {
|
|
break;
|
|
}
|
|
let is_expanded = if focused == Some(idx) {
|
|
expanded.get(&file.path).copied().unwrap_or(true)
|
|
} else {
|
|
file_expanded(file, expanded, oversized)
|
|
};
|
|
gap(&mut out);
|
|
out.extend(file_rows(idx, file, is_expanded, mode));
|
|
}
|
|
|
|
if focused.is_none() && snap.files.len() > shown {
|
|
gap(&mut out);
|
|
out.push(DiffRow::MoreFiles {
|
|
rest: snap.files.len() - shown,
|
|
});
|
|
}
|
|
|
|
if focused.is_none() && !snap.untracked.is_empty() {
|
|
gap(&mut out);
|
|
out.extend(untracked_rows(snap));
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
/// The single synthesized file an untracked file's preview shows.
|
|
///
|
|
/// `usize::MAX` keeps its element ids clear of the real list's.
|
|
pub(crate) fn preview_rows(file: &FileDiff, mode: DiffViewMode) -> Vec<DiffRow> {
|
|
file_rows(usize::MAX, file, true, mode)
|
|
}
|
|
|
|
fn file_rows(index: usize, file: &FileDiff, expanded: bool, mode: DiffViewMode) -> Vec<DiffRow> {
|
|
let expandable =
|
|
!file.binary && (!file.hunks.is_empty() || file.truncated == Some(Truncation::Budget));
|
|
let shown_path = match &file.old_path {
|
|
Some(old) => format!("{old} → {}", file.path),
|
|
None => file.path.clone(),
|
|
};
|
|
let mut rows = vec![DiffRow::FileHeader(FileHead {
|
|
index,
|
|
path: file.path.clone(),
|
|
shown_path,
|
|
status: file.status,
|
|
added: file.added,
|
|
removed: file.removed,
|
|
binary: file.binary,
|
|
expandable,
|
|
expanded,
|
|
})];
|
|
|
|
if expanded && (!file.hunks.is_empty() || file.truncated.is_some()) {
|
|
let path = SharedString::from(file.path.clone());
|
|
for (h, hunk) in file.hunks.iter().enumerate() {
|
|
rows.push(DiffRow::HunkHeader {
|
|
text: hunk.header.clone(),
|
|
leads: h == 0,
|
|
});
|
|
let at = |row| RowAt {
|
|
path: path.clone(),
|
|
id: RowId { hunk: h, row },
|
|
};
|
|
match mode {
|
|
DiffViewMode::Split => rows.extend(
|
|
split_hunk(&hunk.lines)
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(r, row)| DiffRow::Split { row, at: at(r) }),
|
|
),
|
|
DiffViewMode::Unified => rows.extend(
|
|
unified_rows(&hunk.lines)
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(r, row)| DiffRow::Unified { row, at: at(r) }),
|
|
),
|
|
}
|
|
}
|
|
if let Some(reason) = file.truncated {
|
|
rows.push(DiffRow::Truncated(reason));
|
|
}
|
|
}
|
|
|
|
rows
|
|
}
|
|
|
|
fn untracked_rows(snap: &DiffSnapshot) -> Vec<DiffRow> {
|
|
let total = snap.untracked_count();
|
|
let shown = &snap.untracked[..snap.untracked.len().min(MAX_RENDERED_FILES)];
|
|
let mut rows = vec![DiffRow::UntrackedHeader { total }];
|
|
for (index, path) in shown.iter().enumerate() {
|
|
rows.push(DiffRow::Untracked {
|
|
index,
|
|
path: path.clone(),
|
|
});
|
|
}
|
|
if total > shown.len() {
|
|
rows.push(DiffRow::MoreUntracked {
|
|
rest: total - shown.len(),
|
|
});
|
|
}
|
|
rows
|
|
}
|
|
|
|
/// Whether a file's lines show on their own, absent an explicit choice.
|
|
pub(crate) fn file_expanded(
|
|
file: &FileDiff,
|
|
expanded: &HashMap<String, bool>,
|
|
collapse_all: bool,
|
|
) -> bool {
|
|
if let Some(&want) = expanded.get(&file.path) {
|
|
return want;
|
|
}
|
|
!collapse_all && file.added + file.removed <= AUTO_COLLAPSE_LINES
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::terminal::git_diff::{DiffLine, Hunk, LineKind};
|
|
|
|
fn line(kind: LineKind, old: Option<u32>, new: Option<u32>) -> DiffLine {
|
|
DiffLine {
|
|
kind,
|
|
old_no: old,
|
|
new_no: new,
|
|
text: "x".to_string(),
|
|
}
|
|
}
|
|
|
|
/// One removal answered by one addition, wrapped in a context line either
|
|
/// side — four lines unified, three rows split.
|
|
fn hunk() -> Hunk {
|
|
Hunk {
|
|
header: "@@ -1,3 +1,3 @@".to_string(),
|
|
lines: vec![
|
|
line(LineKind::Context, Some(1), Some(1)),
|
|
line(LineKind::Removed, Some(2), None),
|
|
line(LineKind::Added, None, Some(2)),
|
|
line(LineKind::Context, Some(3), Some(3)),
|
|
],
|
|
}
|
|
}
|
|
|
|
fn file(path: &str, hunks: usize) -> FileDiff {
|
|
FileDiff {
|
|
path: path.to_string(),
|
|
old_path: None,
|
|
status: FileStatus::Modified,
|
|
added: 1,
|
|
removed: 1,
|
|
binary: false,
|
|
truncated: None,
|
|
hunks: std::iter::repeat_n(hunk(), hunks).collect(),
|
|
}
|
|
}
|
|
|
|
fn snapshot(files: Vec<FileDiff>) -> DiffSnapshot {
|
|
DiffSnapshot {
|
|
files,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// `(path, hunk, row)` for each line row, in list order.
|
|
fn line_coords(rows: &[DiffRow]) -> Vec<(String, usize, usize)> {
|
|
rows.iter()
|
|
.filter_map(|row| match row {
|
|
DiffRow::Split { at, .. } | DiffRow::Unified { at, .. } => {
|
|
Some((at.path.to_string(), at.id.hunk, at.id.row))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// A row says where it is in the *patch*, not where it is in the list.
|
|
/// Collapsing a file splices the rows below it up the list; a range keyed
|
|
/// on list positions would stay where it was and come away naming other
|
|
/// code.
|
|
#[test]
|
|
fn a_line_row_names_its_file_and_its_place_in_the_hunk() {
|
|
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
|
|
let open = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
|
|
let coords = line_coords(&open);
|
|
assert_eq!(
|
|
coords,
|
|
vec![
|
|
("a.rs".to_string(), 0, 0),
|
|
("a.rs".to_string(), 0, 1),
|
|
("a.rs".to_string(), 0, 2),
|
|
("a.rs".to_string(), 0, 3),
|
|
("b.rs".to_string(), 0, 0),
|
|
("b.rs".to_string(), 0, 1),
|
|
("b.rs".to_string(), 0, 2),
|
|
("b.rs".to_string(), 0, 3),
|
|
]
|
|
);
|
|
|
|
let collapsed = HashMap::from([("a.rs".to_string(), false)]);
|
|
let after = build_rows(&snap, &collapsed, None, DiffViewMode::Unified, false);
|
|
assert_eq!(
|
|
line_coords(&after),
|
|
coords[4..].to_vec(),
|
|
"b.rs's lines moved up the list and kept the coordinates they had"
|
|
);
|
|
}
|
|
|
|
/// Every hunk restarts the row count, so a range that spans two of them is
|
|
/// read hunk by hunk rather than as one run.
|
|
#[test]
|
|
fn each_hunk_numbers_its_own_rows() {
|
|
let snap = snapshot(vec![file("a.rs", 2)]);
|
|
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Split, false);
|
|
let hunks: Vec<_> = line_coords(&rows)
|
|
.into_iter()
|
|
.map(|(_, hunk, row)| (hunk, row))
|
|
.collect();
|
|
assert_eq!(hunks, vec![(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]);
|
|
}
|
|
|
|
fn shape(rows: &[DiffRow]) -> Vec<&'static str> {
|
|
rows.iter()
|
|
.map(|row| match row {
|
|
DiffRow::Gap => "gap",
|
|
DiffRow::Oversized => "oversized",
|
|
DiffRow::FileHeader(_) => "file",
|
|
DiffRow::HunkHeader { .. } => "hunk",
|
|
DiffRow::Split { .. } => "split",
|
|
DiffRow::Unified { .. } => "unified",
|
|
DiffRow::Truncated(_) => "truncated",
|
|
DiffRow::MoreFiles { .. } => "more-files",
|
|
DiffRow::UntrackedHeader { .. } => "untracked-header",
|
|
DiffRow::Untracked { .. } => "untracked",
|
|
DiffRow::MoreUntracked { .. } => "more-untracked",
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn open(paths: [&str; 1]) -> HashMap<String, bool> {
|
|
paths.into_iter().map(|p| (p.to_string(), true)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn an_open_file_flattens_to_a_header_a_hunk_header_and_a_row_per_line() {
|
|
let snap = snapshot(vec![file("a.rs", 1)]);
|
|
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
|
|
assert_eq!(
|
|
shape(&rows),
|
|
["file", "hunk", "unified", "unified", "unified", "unified"]
|
|
);
|
|
|
|
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Split, false);
|
|
assert_eq!(
|
|
shape(&rows),
|
|
["file", "hunk", "split", "split", "split"],
|
|
"the split view pairs the removal with the addition that replaced it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_first_hunk_of_a_file_follows_its_header() {
|
|
let rows = build_rows(
|
|
&snapshot(vec![file("a.rs", 2)]),
|
|
&HashMap::new(),
|
|
None,
|
|
DiffViewMode::Unified,
|
|
false,
|
|
);
|
|
let leads: Vec<bool> = rows
|
|
.iter()
|
|
.filter_map(|r| match r {
|
|
DiffRow::HunkHeader { leads, .. } => Some(*leads),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(
|
|
leads,
|
|
[true, false],
|
|
"the second hunk draws the rule that separates it from the first"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_collapsed_file_is_a_single_row() {
|
|
let snap = snapshot(vec![file("a.rs", 1)]);
|
|
let shut: HashMap<String, bool> = [("a.rs".to_string(), false)].into_iter().collect();
|
|
let rows = build_rows(&snap, &shut, None, DiffViewMode::Unified, false);
|
|
assert_eq!(shape(&rows), ["file"]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_truncation_note_lands_under_the_lines_it_is_about() {
|
|
let mut f = file("a.rs", 1);
|
|
f.truncated = Some(Truncation::Budget);
|
|
let rows = build_rows(
|
|
&snapshot(vec![f]),
|
|
&HashMap::new(),
|
|
None,
|
|
DiffViewMode::Unified,
|
|
false,
|
|
);
|
|
assert_eq!(*shape(&rows).last().unwrap(), "truncated");
|
|
}
|
|
|
|
#[test]
|
|
fn files_are_separated_by_a_gap_and_the_list_never_opens_with_one() {
|
|
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
|
|
let shut: HashMap<String, bool> =
|
|
snap.files.iter().map(|f| (f.path.clone(), false)).collect();
|
|
let rows = build_rows(&snap, &shut, None, DiffViewMode::Unified, false);
|
|
assert_eq!(shape(&rows), ["file", "gap", "file"]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_oversized_banner_leads_and_collapses_what_was_not_asked_for() {
|
|
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
|
|
let rows = build_rows(&snap, &open(["b.rs"]), None, DiffViewMode::Unified, true);
|
|
assert_eq!(
|
|
shape(&rows),
|
|
[
|
|
"oversized",
|
|
"gap",
|
|
"file",
|
|
"gap",
|
|
"file",
|
|
"hunk",
|
|
"unified",
|
|
"unified",
|
|
"unified",
|
|
"unified"
|
|
],
|
|
"the file the reader opened stays open; the other one does not"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_focused_file_is_the_only_one_flattened() {
|
|
let snap = snapshot(vec![file("a.rs", 1), file("b.rs", 1)]);
|
|
let rows = build_rows(
|
|
&snap,
|
|
&HashMap::new(),
|
|
Some(1),
|
|
DiffViewMode::Unified,
|
|
false,
|
|
);
|
|
assert_eq!(
|
|
shape(&rows),
|
|
["file", "hunk", "unified", "unified", "unified", "unified"]
|
|
);
|
|
let DiffRow::FileHeader(head) = &rows[0] else {
|
|
panic!("the focused file leads");
|
|
};
|
|
assert_eq!(head.path, "b.rs");
|
|
assert_eq!(head.index, 1, "the element id still points into `files`");
|
|
}
|
|
|
|
#[test]
|
|
fn the_file_list_is_capped_and_the_tail_gets_one_line() {
|
|
let snap = snapshot(
|
|
(0..MAX_RENDERED_FILES + 25)
|
|
.map(|i| {
|
|
let mut f = file(&format!("f{i}.rs"), 1);
|
|
f.hunks.clear();
|
|
f
|
|
})
|
|
.collect(),
|
|
);
|
|
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
|
|
let files = rows
|
|
.iter()
|
|
.filter(|r| matches!(r, DiffRow::FileHeader(_)))
|
|
.count();
|
|
assert_eq!(files, MAX_RENDERED_FILES);
|
|
assert!(matches!(rows.last(), Some(DiffRow::MoreFiles { rest: 25 })));
|
|
}
|
|
|
|
#[test]
|
|
fn the_untracked_list_is_capped_but_its_count_stays_true() {
|
|
let mut snap = snapshot(Vec::new());
|
|
snap.untracked = (0..MAX_RENDERED_FILES + 5)
|
|
.map(|i| format!("p{i}"))
|
|
.collect();
|
|
snap.untracked_total = 9_000;
|
|
let rows = build_rows(&snap, &HashMap::new(), None, DiffViewMode::Unified, false);
|
|
|
|
assert!(matches!(
|
|
rows.first(),
|
|
Some(DiffRow::UntrackedHeader { total: 9_000 })
|
|
));
|
|
let shown = rows
|
|
.iter()
|
|
.filter(|r| matches!(r, DiffRow::Untracked { .. }))
|
|
.count();
|
|
assert_eq!(shown, MAX_RENDERED_FILES);
|
|
assert!(matches!(
|
|
rows.last(),
|
|
Some(DiffRow::MoreUntracked { rest }) if *rest == 9_000 - MAX_RENDERED_FILES
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_preview_is_one_open_file() {
|
|
let rows = preview_rows(&file("new.md", 1), DiffViewMode::Unified);
|
|
assert_eq!(
|
|
shape(&rows),
|
|
["file", "hunk", "unified", "unified", "unified", "unified"]
|
|
);
|
|
let DiffRow::FileHeader(head) = &rows[0] else {
|
|
panic!("a file opens with its header");
|
|
};
|
|
assert_eq!(
|
|
head.index,
|
|
usize::MAX,
|
|
"its element ids stay clear of the real list's"
|
|
);
|
|
assert!(head.expanded);
|
|
}
|
|
|
|
#[test]
|
|
fn a_rename_shows_both_names_and_a_binary_file_cannot_be_opened() {
|
|
let mut renamed = file("new.rs", 1);
|
|
renamed.old_path = Some("old.rs".to_string());
|
|
let rows = preview_rows(&renamed, DiffViewMode::Unified);
|
|
let DiffRow::FileHeader(head) = &rows[0] else {
|
|
unreachable!()
|
|
};
|
|
assert_eq!(head.shown_path, "old.rs → new.rs");
|
|
assert_eq!(head.path, "new.rs", "the key is still the file itself");
|
|
|
|
let mut binary = file("logo.png", 0);
|
|
binary.binary = true;
|
|
let rows = preview_rows(&binary, DiffViewMode::Unified);
|
|
let DiffRow::FileHeader(head) = &rows[0] else {
|
|
unreachable!()
|
|
};
|
|
assert!(!head.expandable);
|
|
}
|
|
|
|
/// The threshold that decides whether a file opens on its own, kept where
|
|
/// the flattening that reads it lives.
|
|
#[test]
|
|
fn a_file_over_the_line_count_opens_only_when_asked() {
|
|
let mut big = file("big.rs", 1);
|
|
big.added = AUTO_COLLAPSE_LINES + 1;
|
|
let none = HashMap::new();
|
|
assert!(!file_expanded(&big, &none, false));
|
|
assert!(file_expanded(&big, &open(["big.rs"]), true));
|
|
assert!(file_expanded(&file("small.rs", 1), &none, false));
|
|
}
|
|
}
|