Squash of 27 work commits (Sep 3–12):aa907caplatform: a frame trace on both desktop backends, and a window that cannot present stops ticking at 600 Hze3abf4dmap: the hosted-tile cache keeps the same ~2x-visible margin as the local one — a budget pinned at the visible set evicted the trailing edge of every pan on the next frame and refetched it a moment later8d542feplatform: a child pass orphaned by its attaching draw list is no longer painted — the window's gauss_scene pass stayed a live_with_parent child after the map went flat and re-encoded a frozen 400-item list every pan frame with stale geometry ids (new tile meshes × old instance counts, tens of millions of triangles into a texture nobody read); make_child_pass records the recording list + redraw id, stale passes are skipped, a cached View re-attaches on a cache hit; tests for the orphan gate and the pool generation contractbb49fe6map + platform: retained per-tile draw lists — each resident tile owns one DrawList2d per carto pass (fill, casing, stroke, icon, icon-high, shadow) and the label glyph batches are retained the same way, recorded when the bake, LOD ring, fringe/icon gates, flat/tilted or clip change and re-attached otherwise; a pan/zoom/tilt frame pushes this frame's uniforms onto the retained calls (DrawVars::update_uniforms_on_area, resolved slot table) and uploads zero instance bytes; the tilted per-pass depth is a pass_depth uniform; a held list's zbias resolves at entry (zbias_hold in every backend); the shimmer heartbeat patches shiny_time in place without a redraw; a freed/reused sub-list id is skipped by every draw-tree walker and the mask list re-records empty on the flat transition (contract test). Web pan tail 1,098 → 20 MiB/s, flat pan 2.4 → 0.43 MiB/frame; Metal grabs within the run-to-run noise floor 313265d Studio code atlas: geometry code views, live filter, 3D size lens, lanes as terminals 9c4e0cc Studio code atlas: performance round — retained uploads, worker labels, exact search, no forks 7235d7e Studio code atlas: stall fix, GPU working set, lens hard switch, filter masks, parallel index 56a6da5 platform: per-pass GPU counter timing on Metal; retained publications replace in place 72e2443 platform: present-path trace (1 Hz cause histograms), bounded drawable wait and retirement on macOS 1c5d583 platform: bounded retained maintenance on empty paint beats; republish actual backend debt edfed73 platform: retained residency high/low water and hysteresis; no distance eviction without pressure acab6a0 platform: critical upload class serves present-blocking items first; identical immediate re-records upload nothing 906c774 platform: uniform_range on DrawVars and patch_retained_uniforms on retained draw lists c29031c platform/draw/widgets: heap-keyed script resources and RecordingBuffer draw items — the files today's commits depend on cd0964f platform headless: homogeneous near-plane clipping before the perspective divide 61d424d platform: release retained bindings of released textures so pool evictions complete a00287a platform: texture-tile cache support — per-item instance ranges, painted pass receipts, display-dpi pass uniform, retained render targets, present gate on the drawable pool 74b63be platform: retained upload floor reverted, unconfirmed presents counted c03fcc5 platform: tile-cache round 3 support — O(1) demand on re-recorded lists, evictions counter, allocated_size, lost-button release, Vec2d::round e515d75 platform: shared instance publications — the DL-0/DL-1 contract, additive beside the retained path 142a337 platform: shared instance publications — close the seven review items (DL-1b) 6811a19 platform: Debug for SharedInstances, WeakSharedInstances and PublishReceipt c061e47 platform: Metal draws are resident by construction — the hole path and its gates are gone (DL-2) 0bdbb29 platform: drop the unreachable InstancesNotResident present cause de3c868 platform: `drawlist` trace names the holder of a stale draw-list id; the per-frame upload line moves to `gpu.upload` 721c3d8 platform: publication backends — Metal per-publication backings, lease-keyed uniform ring, receipts on every backend, Vulkan draws attached items (DL-3) 393de54 platform: integrated deletion — the draw-list system is generic again (DL-5) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
841 lines
33 KiB
Rust
841 lines
33 KiB
Rust
use super::*;
|
||
use crate::{
|
||
decoration::DecorationSet,
|
||
document::{AnchorRange, CodeDocument, PreparedDocument},
|
||
history::EditKind,
|
||
selection::SelectionSet,
|
||
session::{PreparedView, PreparedViewError, SessionId},
|
||
text::{Change, Drift, Edit, Length},
|
||
};
|
||
|
||
fn pos(line_index: usize, byte_index: usize) -> Position {
|
||
Position {
|
||
line_index,
|
||
byte_index,
|
||
}
|
||
}
|
||
|
||
fn doc(source: &str) -> CodeDocument {
|
||
CodeDocument::new(source.into(), DecorationSet::default())
|
||
}
|
||
|
||
fn edit(document: &CodeDocument, change: Change) {
|
||
document.edit_selections(
|
||
SessionId::default(),
|
||
EditKind::Other,
|
||
&SelectionSet::new(),
|
||
&Settings::default(),
|
||
|mut editor, _, _| {
|
||
editor.apply_edit(Edit {
|
||
change: change.clone(),
|
||
drift: Drift::Before,
|
||
});
|
||
},
|
||
);
|
||
}
|
||
|
||
fn editor() -> (Cx, CodeEditor) {
|
||
let mut cx = Cx::new(Box::new(|_, _| {}));
|
||
let editor = cx.with_vm(|vm| {
|
||
makepad_widgets::script_mod(vm);
|
||
crate::script_mod(vm);
|
||
vm.bx.captured_errors = Some(Vec::new());
|
||
let value = script_eval!(vm, { use mod.widgets.* CodeEditor{} });
|
||
let editor = CodeEditor::script_from_value(vm, value);
|
||
assert!(vm.take_errors().is_empty());
|
||
editor
|
||
});
|
||
(cx, editor)
|
||
}
|
||
|
||
fn measured_editor() -> (Cx, CodeEditor) {
|
||
let (cx, mut editor) = editor();
|
||
editor.cell_size = dvec2(8.0, 16.0);
|
||
editor.ascent = 10.0;
|
||
editor.descent = 3.0;
|
||
editor.cell_offset_y = 1.5;
|
||
editor.pad_left_top = dvec2(0.0, 0.0);
|
||
editor.show_gutter = false;
|
||
editor.viewport_rect = Rect {
|
||
pos: dvec2(100.0, 50.0),
|
||
size: dvec2(600.0, 300.0),
|
||
};
|
||
editor.unscrolled_rect = editor.viewport_rect;
|
||
(cx, editor)
|
||
}
|
||
|
||
#[test]
|
||
fn anchors_follow_shared_insertions_and_deletion() {
|
||
let document = doc("abcdef");
|
||
let before = document.create_anchor(pos(0, 2), Drift::Before);
|
||
let after = document.create_anchor(pos(0, 2), Drift::After);
|
||
let clone = document.clone();
|
||
edit(&clone, Change::Insert(pos(0, 0), "x".into()));
|
||
assert_eq!(document.resolve_anchor(before), Some(pos(0, 3)));
|
||
edit(&clone, Change::Insert(pos(0, 3), "yz".into()));
|
||
assert_eq!(document.resolve_anchor(before), Some(pos(0, 3)));
|
||
assert_eq!(document.resolve_anchor(after), Some(pos(0, 5)));
|
||
edit(&clone, Change::Insert(pos(0, 8), "z".into()));
|
||
assert_eq!(document.resolve_anchor(after), Some(pos(0, 5)));
|
||
edit(
|
||
&clone,
|
||
Change::Delete(
|
||
pos(0, 1),
|
||
Length {
|
||
line_count: 0,
|
||
byte_count: 6,
|
||
},
|
||
),
|
||
);
|
||
assert_eq!(document.resolve_anchor(before), Some(pos(0, 1)));
|
||
assert_eq!(document.resolve_anchor(after), Some(pos(0, 1)));
|
||
assert_eq!(doc("abc").resolve_anchor(before), None);
|
||
document.remove_anchor(before);
|
||
assert_eq!(clone.resolve_anchor(before), None);
|
||
}
|
||
|
||
#[test]
|
||
fn anchors_follow_multiline_edits_and_shared_undo() {
|
||
let document = doc("abc\ndef\nghi");
|
||
let anchor = document.create_anchor(pos(2, 1), Drift::After);
|
||
let mut session = CodeSession::new(document.clone());
|
||
session.set_selection(
|
||
pos(0, 0),
|
||
Affinity::After,
|
||
SelectionMode::Simple,
|
||
NewGroup::Yes,
|
||
);
|
||
session.paste("z\n".into());
|
||
session.handle_changes();
|
||
assert_eq!(document.resolve_anchor(anchor), Some(pos(3, 1)));
|
||
assert!(session.undo());
|
||
session.handle_changes();
|
||
assert_eq!(document.resolve_anchor(anchor), Some(pos(2, 1)));
|
||
edit(
|
||
&document,
|
||
Change::Delete(
|
||
pos(0, 2),
|
||
Length {
|
||
line_count: 2,
|
||
byte_count: 2,
|
||
},
|
||
),
|
||
);
|
||
assert_eq!(document.resolve_anchor(anchor), Some(pos(0, 2)));
|
||
}
|
||
|
||
#[test]
|
||
fn range_retains_document_positions_and_follows_shared_edits() {
|
||
let document = doc("prefix\nab界cd\nefgh\ntail");
|
||
let anchors = AnchorRange {
|
||
start: document.create_anchor(pos(1, 2), Drift::Before),
|
||
end: document.create_anchor(pos(2, 2), Drift::After),
|
||
};
|
||
let mut session = CodeSession::new(document.clone());
|
||
session.set_view_range(Some(anchors));
|
||
assert_eq!(session.layout().view_line_range(), 1..3);
|
||
assert_eq!(session.layout().line_byte_range(1), 2..7);
|
||
assert_eq!(session.layout().line_byte_range(2), 0..2);
|
||
assert_eq!(session.layout().line(1).y(), 0.0);
|
||
assert_eq!(session.layout().height(), 2.0);
|
||
assert_eq!(
|
||
session
|
||
.layout()
|
||
.logical_to_normalized_position(pos(1, 5), Affinity::After),
|
||
(4.0, 0.0)
|
||
);
|
||
edit(&document, Change::Insert(pos(0, 0), "new\n".into()));
|
||
edit(&document, Change::Insert(pos(2, 5), "X".into()));
|
||
session.handle_changes();
|
||
assert_eq!(session.view_range(), Some(pos(2, 2)..pos(3, 2)));
|
||
assert_eq!(session.layout().view_line_range(), 2..4);
|
||
assert_eq!(session.layout().line(2).y(), 0.0);
|
||
session.set_view_range(None);
|
||
assert_eq!(session.layout().line(2).y(), 2.0);
|
||
}
|
||
|
||
#[test]
|
||
fn range_excludes_end_line_and_clamps_selection() {
|
||
let document = doc("one\ntwo\nthree");
|
||
let mut session = CodeSession::new(document.clone());
|
||
session.set_view_range(Some(AnchorRange {
|
||
start: document.create_anchor(pos(1, 1), Drift::Before),
|
||
end: document.create_anchor(pos(2, 0), Drift::After),
|
||
}));
|
||
assert_eq!(session.layout().view_line_range(), 1..2);
|
||
assert_eq!(session.layout().height(), 1.0);
|
||
session.set_selection(pos(0, 0), Affinity::After, SelectionMode::All, NewGroup::No);
|
||
assert_eq!(session.selections()[0].start(), pos(1, 1));
|
||
assert_eq!(session.selections()[0].end(), pos(2, 0));
|
||
edit(
|
||
&document,
|
||
Change::Delete(
|
||
pos(0, 0),
|
||
Length {
|
||
line_count: 2,
|
||
byte_count: 2,
|
||
},
|
||
),
|
||
);
|
||
session.handle_changes();
|
||
assert_eq!(session.view_range(), Some(pos(0, 0)..pos(0, 0)));
|
||
assert_eq!(session.layout().height(), 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn prepared_document_and_view_are_send_and_attach_without_relayout() {
|
||
fn assert_send<T: Send>() {}
|
||
assert_send::<PreparedDocument>();
|
||
assert_send::<PreparedView>();
|
||
let (prepared, view) = std::thread::spawn(|| {
|
||
let prepared = CodeDocument::prepare("// hello\nlet v = vec![界];".into());
|
||
let view = CodeSession::prepare_view(
|
||
&prepared,
|
||
Some(pos(1, 0)..pos(1, prepared.as_text().as_lines()[1].len())),
|
||
);
|
||
(prepared, view)
|
||
})
|
||
.join()
|
||
.unwrap();
|
||
let digest = prepared.digest();
|
||
let text_pointer = prepared.as_text().as_lines()[1].as_ptr();
|
||
let document = CodeDocument::from_prepared(prepared);
|
||
assert_eq!(document.digest(), digest);
|
||
assert_eq!(document.as_text().as_lines()[1].as_ptr(), text_pointer);
|
||
let session = CodeSession::from_prepared(document.clone(), view).unwrap();
|
||
assert_eq!(session.layout().line(1).y(), 0.0);
|
||
assert_eq!(
|
||
session.layout().line(1).width(),
|
||
document.as_text().as_lines()[1].column_count() as f64
|
||
);
|
||
assert!(document.layout().tokens[1]
|
||
.iter()
|
||
.any(|token| token.kind == TokenKind::Macro));
|
||
}
|
||
|
||
#[test]
|
||
fn prepared_view_rejects_wrong_digest_edited_version_and_invalid_range() {
|
||
let prepared = CodeDocument::prepare("abc".into());
|
||
let view = CodeSession::prepare_view(&prepared, None);
|
||
assert_eq!(
|
||
CodeSession::from_prepared(doc("xyz"), view.clone()).unwrap_err(),
|
||
PreparedViewError::Stale
|
||
);
|
||
let document = CodeDocument::from_prepared(prepared.clone());
|
||
document.replace("abc".into());
|
||
assert_eq!(document.digest(), prepared.digest());
|
||
assert_eq!(
|
||
CodeSession::from_prepared(document, view).unwrap_err(),
|
||
PreparedViewError::Stale
|
||
);
|
||
let invalid = CodeSession::prepare_view(&prepared, Some(pos(0, 2)..pos(0, 8)));
|
||
assert_eq!(
|
||
CodeSession::from_prepared(CodeDocument::from_prepared(prepared), invalid).unwrap_err(),
|
||
PreparedViewError::InvalidRange
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hit_test_matches_wide_utf8_cells_scroll_and_range() {
|
||
let (_cx, mut editor) = measured_editor();
|
||
let document = doc("hidden\na界\tz!");
|
||
let mut session = CodeSession::new(document.clone());
|
||
session.set_view_range(Some(AnchorRange {
|
||
start: document.create_anchor(pos(1, 1), Drift::Before),
|
||
end: document.create_anchor(pos(1, 6), Drift::After),
|
||
}));
|
||
let rect = editor.position_rect(&session, pos(1, 1)).unwrap();
|
||
assert_eq!(rect.pos, dvec2(8.0, 0.0));
|
||
assert_eq!(rect.size, dvec2(16.0, 16.0));
|
||
let hit = editor
|
||
.hit_test(&session, rect.pos + rect.size * 0.75)
|
||
.unwrap();
|
||
assert_eq!(hit.position, pos(1, 4));
|
||
assert_eq!(hit.grapheme_range, pos(1, 1)..pos(1, 4));
|
||
assert_eq!(hit.rect, rect);
|
||
assert!(editor.position_rect(&session, pos(1, 2)).is_none());
|
||
assert!(editor.position_rect(&session, pos(1, 6)).is_none());
|
||
assert!(editor.hit_test(&session, dvec2(4.0, 8.0)).is_none());
|
||
assert!(editor.hit_test(&session, dvec2(100.0, 8.0)).is_none());
|
||
// Draw's viewport origin includes scrolling; public coordinates are outer-local.
|
||
editor.viewport_rect.pos.x -= 4.0;
|
||
assert_eq!(
|
||
editor.position_rect(&session, pos(1, 1)).unwrap().pos.x,
|
||
4.0
|
||
);
|
||
assert_eq!(
|
||
editor.hit_test(&session, dvec2(5.0, 8.0)).unwrap().position,
|
||
pos(1, 1)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn base_font_scale_multiplies_fold_scale_without_frame_contamination() {
|
||
let (_cx, mut editor) = measured_editor();
|
||
let session = CodeSession::new(doc(" folded\nnext"));
|
||
let before = editor.metrics(&session);
|
||
editor.set_font_scale(0.5);
|
||
let scaled = editor.metrics(&session);
|
||
assert_eq!(scaled.line_advance, before.line_advance * 0.5);
|
||
assert_eq!(scaled.column_advance, before.column_advance * 0.5);
|
||
assert_eq!(scaled.ascent, before.ascent * 0.5);
|
||
assert_eq!(scaled.descent, before.descent * 0.5);
|
||
session.fold();
|
||
while session.update_folds() {}
|
||
assert_eq!(
|
||
editor.position_rect(&session, pos(0, 8)).unwrap().size.y,
|
||
0.8
|
||
);
|
||
editor.draw_text.font_scale = editor.base_font_scale * 0.1;
|
||
editor.draw_gutter.font_scale = editor.base_font_scale * 0.1;
|
||
editor.reset_draw_font_scale();
|
||
assert_eq!(editor.draw_text.font_scale, 0.5);
|
||
assert_eq!(editor.draw_gutter.font_scale, 0.5);
|
||
assert_eq!(editor.metrics(&session), scaled);
|
||
editor.set_font_scale(f32::NAN);
|
||
assert_eq!(editor.metrics(&session), scaled);
|
||
}
|
||
|
||
#[test]
|
||
fn wrapped_range_rebases_first_visible_row_and_keeps_global_geometry() {
|
||
let (_cx, editor) = measured_editor();
|
||
let document = doc("one two three four\nlast");
|
||
let mut session = CodeSession::new(document.clone());
|
||
session.set_wrap_column(Some(8));
|
||
session.set_view_range(Some(AnchorRange {
|
||
start: document.create_anchor(pos(0, 8), Drift::Before),
|
||
end: document.create_anchor(pos(0, 13), Drift::After),
|
||
}));
|
||
assert_eq!(
|
||
editor.position_rect(&session, pos(0, 8)).unwrap().pos.y,
|
||
0.0
|
||
);
|
||
assert_eq!(session.layout().height(), 1.0);
|
||
assert_eq!(session.layout().width(), 5.0);
|
||
}
|
||
|
||
#[test]
|
||
fn macro_and_attribute_tokens_respect_lexical_context_and_incremental_edits() {
|
||
let document = doc("vec![] foo ![]\n#![cfg(any(\n feature = \"x]y\", /* ] */ test\n))]\nfn f() {} // #[x] foo!\n\"#[x] foo!\"");
|
||
let kinds = document.layout().tokens[0]
|
||
.iter()
|
||
.map(|token| token.kind)
|
||
.collect::<Vec<_>>();
|
||
assert_eq!(kinds[0], TokenKind::Macro);
|
||
assert!(kinds.contains(&TokenKind::Identifier));
|
||
for line in 1..=3 {
|
||
assert!(document.layout().tokens[line]
|
||
.iter()
|
||
.all(|token| token.kind == TokenKind::Attribute));
|
||
}
|
||
assert_eq!(document.layout().tokens[4][0].kind, TokenKind::OtherKeyword);
|
||
assert_eq!(document.layout().tokens[5][0].kind, TokenKind::String);
|
||
edit(
|
||
&document,
|
||
Change::Delete(
|
||
pos(1, 0),
|
||
Length {
|
||
line_count: 0,
|
||
byte_count: 1,
|
||
},
|
||
),
|
||
);
|
||
assert!(document.layout().tokens[2]
|
||
.iter()
|
||
.any(|token| token.kind != TokenKind::Attribute));
|
||
for (line, tokens) in document
|
||
.as_text()
|
||
.as_lines()
|
||
.iter()
|
||
.zip(&document.layout().tokens)
|
||
{
|
||
assert_eq!(
|
||
line.len(),
|
||
tokens.iter().map(|token| token.len).sum::<usize>()
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn read_only_blocks_return_typing_paste_ime_undo_redo_cut_and_drop() {
|
||
assert_mutation_events_refused(doc("unchanged"), true);
|
||
let prepared = CodeDocument::prepare_diff(&[
|
||
diff_row(DiffRowKind::Removed, Some(0), None, 1),
|
||
], "unchanged", "").unwrap();
|
||
assert_mutation_events_refused(CodeDocument::from_prepared_diff(prepared), false);
|
||
}
|
||
|
||
fn assert_mutation_events_refused(document: CodeDocument, explicit_read_only: bool) {
|
||
use std::sync::{Arc, Mutex};
|
||
let (mut cx, mut editor) = editor();
|
||
let mut session = CodeSession::new(document);
|
||
record_editor(&mut cx, &mut editor, &mut session);
|
||
editor.set_key_focus(&mut cx);
|
||
cx.action(());
|
||
cx.handle_actions();
|
||
assert!(cx.has_key_focus(editor.scroll_bars.area()));
|
||
session.set_selection(pos(0, 0), Affinity::After, SelectionMode::All, NewGroup::No);
|
||
let copy = TextClipboardEvent { response: Default::default() };
|
||
editor.handle_event(&mut cx, &Event::TextCopy(copy.clone()), &mut Scope::empty(), &mut session);
|
||
assert_eq!(copy.response.borrow().as_deref(), Some("unchanged"));
|
||
editor.blink_timer = cx.start_timeout(0.5);
|
||
editor.set_read_only(&mut cx, explicit_read_only);
|
||
if explicit_read_only { assert_eq!(editor.blink_timer.0, 0); }
|
||
let mut events = Vec::new();
|
||
for key_code in [
|
||
KeyCode::ReturnKey,
|
||
KeyCode::Tab,
|
||
KeyCode::Backspace,
|
||
KeyCode::Delete,
|
||
KeyCode::KeyZ,
|
||
KeyCode::KeyY,
|
||
KeyCode::KeyX,
|
||
KeyCode::KeyV,
|
||
] {
|
||
events.push(Event::KeyDown(KeyEvent {
|
||
key_code,
|
||
modifiers: KeyModifiers {
|
||
control: true,
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
}));
|
||
}
|
||
for (was_paste, replace_last) in [(false, false), (true, false), (false, true)] {
|
||
events.push(Event::TextInput(TextInputEvent {
|
||
input: "change".into(),
|
||
was_paste,
|
||
replace_last,
|
||
..Default::default()
|
||
}));
|
||
}
|
||
events.push(Event::TextRangeReplace(TextRangeReplaceEvent {
|
||
start: 0,
|
||
end: 1,
|
||
text: "IME".into(),
|
||
replaced_text: None,
|
||
fallback_to_insert: true,
|
||
}));
|
||
events.push(Event::TextCut(TextClipboardEvent {
|
||
response: Default::default(),
|
||
}));
|
||
events.push(Event::Drop(DropEvent {
|
||
modifiers: KeyModifiers::default(),
|
||
handled: Arc::new(Mutex::new(false)),
|
||
abs: dvec2(0.0, 0.0),
|
||
items: Arc::new(vec![DragItem::String {
|
||
value: "drop".into(),
|
||
internal_id: None,
|
||
}]),
|
||
}));
|
||
for event in events {
|
||
assert!(CodeEditor::is_mutation_event(&event));
|
||
assert!(editor
|
||
.handle_event(&mut cx, &event, &mut Scope::empty(), &mut session)
|
||
.is_empty());
|
||
assert_eq!(session.document().as_text().to_string(), "unchanged");
|
||
assert_eq!(session.document().version(), 0);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn steady_caret_never_rearms_blink_and_read_only_cancels_pending_timeout() {
|
||
let (mut cx, mut editor) = editor();
|
||
editor.reset_cursor_blinker(&mut cx);
|
||
assert_ne!(editor.blink_timer.0, 0);
|
||
editor.set_caret_policy(CaretPolicy::Steady);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
editor.reset_cursor_blinker(&mut cx);
|
||
assert_eq!(editor.retired_blink_timer.0, 0);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
editor.set_caret_policy(CaretPolicy::Blink);
|
||
editor.reset_cursor_blinker(&mut cx);
|
||
assert_ne!(editor.blink_timer.0, 0);
|
||
editor.set_read_only(&mut cx, true);
|
||
editor.reset_cursor_blinker(&mut cx);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn content_opacity_is_clamped_without_changing_background_or_theme() {
|
||
let (_cx, mut editor) = editor();
|
||
let bg = editor.draw_bg.color;
|
||
let gutter = editor.draw_gutter.color;
|
||
let text = editor.token_colors.identifier;
|
||
editor.set_content_opacity(0.25);
|
||
assert_eq!(editor.content_opacity, 0.25);
|
||
assert_eq!(editor.draw_bg.color, bg);
|
||
assert_eq!(editor.draw_gutter.color, gutter);
|
||
assert_eq!(editor.token_colors.identifier, text);
|
||
editor.set_content_opacity(-1.0);
|
||
assert_eq!(editor.content_opacity, 0.0);
|
||
editor.set_content_opacity(2.0);
|
||
editor.set_content_opacity(f32::NAN);
|
||
assert_eq!(editor.content_opacity, 1.0);
|
||
}
|
||
|
||
fn record_editor(cx: &mut Cx, editor: &mut CodeEditor, session: &mut CodeSession) {
|
||
let pass = DrawPass::new(cx);
|
||
pass.set_size(cx, dvec2(480.0, 200.0));
|
||
let mut list = DrawList2d::new(cx);
|
||
let event = DrawEvent::default();
|
||
let mut draw = CxDraw::new(cx, &event);
|
||
let mut cx = Cx2d::new(&mut draw);
|
||
cx.begin_pass(&pass, Some(1.0));
|
||
list.begin_always(&mut cx);
|
||
cx.begin_root_turtle(dvec2(480.0, 200.0), Layout::default());
|
||
editor.draw_walk_editor(&mut cx, session, Walk::fixed(480.0, 200.0));
|
||
cx.end_turtle();
|
||
list.end(&mut cx);
|
||
cx.end_pass(&pass);
|
||
}
|
||
|
||
#[test]
|
||
fn recorded_content_opacity_and_folded_last_line_keep_metrics_stable() {
|
||
let (mut cx, mut editor) = editor();
|
||
let mut session = CodeSession::new(doc("first\n folded"));
|
||
session.fold();
|
||
while session.update_folds() {}
|
||
session.set_selection(pos(0, 0), Affinity::After, SelectionMode::All, NewGroup::No);
|
||
editor.set_font_scale(0.5);
|
||
editor.set_content_opacity(0.25);
|
||
editor.set_caret_policy(CaretPolicy::Steady);
|
||
record_editor(&mut cx, &mut editor, &mut session);
|
||
let metrics = editor.metrics(&session);
|
||
assert!(metrics.line_advance > 0.0);
|
||
for vars in [
|
||
&editor.draw_selection.draw_vars,
|
||
&editor.draw_cursor.draw_vars,
|
||
&editor.draw_cursor_bg.draw_vars,
|
||
] {
|
||
let mut opacity = [-1.0];
|
||
vars.get_instance(&mut cx, live_id!(content_opacity), &mut opacity);
|
||
assert_eq!(opacity, [0.25]);
|
||
}
|
||
record_editor(&mut cx, &mut editor, &mut session);
|
||
assert_eq!(editor.metrics(&session), metrics);
|
||
assert_eq!(editor.draw_text.font_scale, 0.5);
|
||
assert_eq!(editor.draw_gutter.font_scale, 0.5);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
}
|
||
|
||
fn grid_column(session: &CodeSession, byte_index: usize) -> usize {
|
||
session
|
||
.layout()
|
||
.line(0)
|
||
.logical_to_grid_position(byte_index, Affinity::After)
|
||
.1
|
||
}
|
||
|
||
#[test]
|
||
fn tab_advances_to_the_next_stop() {
|
||
use crate::char::CharExt;
|
||
assert_eq!('\t'.column_count(), 1);
|
||
assert_eq!('\t'.column_count_at(0, 4), 4);
|
||
assert_eq!('\t'.column_count_at(2, 4), 2);
|
||
assert_eq!('\t'.column_count_at(4, 4), 4);
|
||
assert_eq!('f'.column_count_at(0, 4), 1);
|
||
assert_eq!('界'.column_count(), 2);
|
||
|
||
let session = CodeSession::new(doc("\tfoo"));
|
||
assert_eq!(grid_column(&session, 1), 4, "f in \\tfoo is at column 4");
|
||
|
||
let mut session = CodeSession::new(doc("ab\tc"));
|
||
assert_eq!(grid_column(&session, 3), 4, "c in ab\\tc is at column 4 with tab width 4");
|
||
session.set_tab_column_count(8);
|
||
assert_eq!(grid_column(&session, 3), 8, "c in ab\\tc is at column 8 with tab width 8");
|
||
|
||
let session = CodeSession::new(doc("\tfoo"));
|
||
session.move_right(true);
|
||
let cursor = session.selections()[0].cursor;
|
||
assert_eq!(cursor.position, pos(0, 1));
|
||
assert_eq!(
|
||
grid_column(&session, cursor.position.byte_index),
|
||
4,
|
||
"cursor-right over a tab jumps to the tab stop"
|
||
);
|
||
|
||
let (_cx, editor) = measured_editor();
|
||
let session = CodeSession::new(doc("\tfoo"));
|
||
let hit = editor.hit_test(&session, dvec2(2.0, 8.0)).unwrap();
|
||
assert_eq!(hit.position, pos(0, 0));
|
||
assert_eq!(
|
||
grid_column(&session, hit.position.byte_index),
|
||
0,
|
||
"hit-test at pixel column 2 of a leading tab selects column 0"
|
||
);
|
||
assert_eq!(hit.rect.size.x, 32.0);
|
||
}
|
||
|
||
use crate::{
|
||
decoration::{Decoration, DecorationType, PreparedDecorationsError},
|
||
diff::{DiffEndpoint, DiffRowKind, DiffRowSpec, GutterMode, LineTerminator, PrepareDiffError, PreparedDiffDocument},
|
||
document::DocumentMutationError,
|
||
};
|
||
|
||
fn diff_row(kind: DiffRowKind, old_line: Option<u32>, new_line: Option<u32>, hunk: u32) -> DiffRowSpec {
|
||
DiffRowSpec { kind, old_line, new_line, hunk }
|
||
}
|
||
|
||
fn attach_diff(prepared: PreparedDiffDocument) -> CodeSession {
|
||
let view = CodeSession::prepare_view(prepared.document(), None);
|
||
CodeSession::from_prepared(CodeDocument::from_prepared_diff(prepared), view).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn diff_endpoint_token_context_survives_removed_multiline_comment() {
|
||
let old = "/* removed\nlet retained = 1;\n*/\nfn after() {}\n";
|
||
let new = "let retained = 1;\nfn after() {}\n";
|
||
let specs = [
|
||
diff_row(DiffRowKind::Removed, Some(0), None, 1),
|
||
diff_row(DiffRowKind::Equal, Some(1), Some(0), 1),
|
||
diff_row(DiffRowKind::Removed, Some(2), None, 2),
|
||
diff_row(DiffRowKind::Equal, Some(3), Some(1), 2),
|
||
];
|
||
let prepared = CodeDocument::prepare_diff(&specs, old, new).unwrap();
|
||
let old_tokens = CodeDocument::prepare(old.into());
|
||
let new_tokens = CodeDocument::prepare(new.into());
|
||
for (row, spec) in specs.iter().enumerate() {
|
||
let expected = if let Some(line) = spec.new_line {
|
||
&new_tokens.layout().tokens[line as usize]
|
||
} else {
|
||
&old_tokens.layout().tokens[spec.old_line.unwrap() as usize]
|
||
};
|
||
assert_eq!(&prepared.document().layout().tokens[row], expected);
|
||
}
|
||
assert_eq!(prepared.document().layout().tokens[1][0].kind, TokenKind::OtherKeyword);
|
||
assert_eq!(prepared.document().layout().tokens[2][0].kind, TokenKind::Comment);
|
||
let combined = CodeDocument::prepare(prepared.as_text().clone());
|
||
assert_eq!(combined.layout().tokens[1][0].kind, TokenKind::Comment);
|
||
let tokens = prepared.document().layout().tokens.clone();
|
||
let mut session = attach_diff(prepared);
|
||
session.handle_changes();
|
||
assert_eq!(session.document().layout().tokens, tokens);
|
||
}
|
||
|
||
#[test]
|
||
fn diff_equal_terminators_refine_and_reconstruct_both_endpoints() {
|
||
let old = "α\t界\r\n\r\nlast\r\n";
|
||
let new = "α\t界\n\nlast";
|
||
let rows = (0..3).map(|line| diff_row(DiffRowKind::Equal, Some(line), Some(line), 7)).collect::<Vec<_>>();
|
||
let prepared = CodeDocument::prepare_diff(&rows, old, new).unwrap();
|
||
assert_eq!(prepared.row_count(), 6);
|
||
assert_eq!(prepared.row_count(), prepared.as_text().as_lines().len());
|
||
assert!(!prepared.as_text().to_string().contains('\r'));
|
||
assert_eq!(prepared.reconstruct(DiffEndpoint::Old).as_bytes(), old.as_bytes());
|
||
assert_eq!(prepared.reconstruct(DiffEndpoint::New).as_bytes(), new.as_bytes());
|
||
let meta = prepared.metadata();
|
||
assert!(meta.old_final_newline);
|
||
assert!(!meta.new_final_newline);
|
||
assert_eq!(meta.old_to_row, [0, 2, 4]);
|
||
assert_eq!(meta.new_to_row, [1, 3, 5]);
|
||
assert_eq!(meta.hunks.len(), 1);
|
||
assert_eq!((meta.hunks[0].removed, meta.hunks[0].added), (3, 3));
|
||
assert!(meta.rows.iter().all(|row| row.gutter_mark == Some(DecorationType::DiffChangedGutter)));
|
||
assert_eq!(meta.rows[0].old.as_ref().unwrap().terminator, LineTerminator::CrLf);
|
||
for row in &meta.rows {
|
||
for (source, text, map) in [(row.old.as_ref(), old, &meta.old_to_row), (row.new.as_ref(), new, &meta.new_to_row)] {
|
||
if let Some(source) = source {
|
||
let display_row = map[source.line as usize];
|
||
assert_eq!(&text[source.bytes.clone()], prepared.as_text().as_lines()[display_row]);
|
||
}
|
||
}
|
||
}
|
||
let session = attach_diff(prepared);
|
||
assert_eq!(session.layout().height(), 6.0);
|
||
assert_eq!(session.layout().line(0).width(), "α\t界".column_count_at(0, 4) as f64);
|
||
assert_eq!(grid_column(&session, "α\t".len()), 4);
|
||
assert_eq!(grid_column(&session, "α\t界".len()), 6);
|
||
}
|
||
|
||
#[test]
|
||
fn diff_empty_sentinel_and_final_newline_do_not_invent_source_rows() {
|
||
let empty = CodeDocument::prepare_diff(&[], "", "").unwrap();
|
||
assert_eq!(empty.row_count(), 1);
|
||
assert_eq!(empty.metadata().rows[0].kind, None);
|
||
assert!(empty.metadata().old_to_row.is_empty());
|
||
assert!(empty.metadata().new_to_row.is_empty());
|
||
assert!(empty.decorations().is_empty());
|
||
assert_eq!(empty.reconstruct(DiffEndpoint::Old), "");
|
||
assert_eq!(attach_diff(empty).layout().height(), 1.0);
|
||
for (old, new, kind, old_line, new_line) in [
|
||
("", "\r\n", DiffRowKind::Added, None, Some(0)),
|
||
("\n", "", DiffRowKind::Removed, Some(0), None),
|
||
("bare\r", "", DiffRowKind::Removed, Some(0), None),
|
||
("x\n", "x\n", DiffRowKind::Equal, Some(0), Some(0)),
|
||
] {
|
||
let prepared = CodeDocument::prepare_diff(&[diff_row(kind, old_line, new_line, 0)], old, new).unwrap();
|
||
assert_eq!(prepared.row_count(), 1);
|
||
assert_eq!(prepared.reconstruct(DiffEndpoint::Old), old);
|
||
assert_eq!(prepared.reconstruct(DiffEndpoint::New), new);
|
||
let session = attach_diff(prepared);
|
||
assert_eq!(session.document().as_text().as_lines().len(), 1);
|
||
assert_eq!(session.layout().height(), 1.0);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diff_preparation_rejects_incomplete_or_invalid_maps() {
|
||
assert_eq!(CodeDocument::prepare_diff(&[], "x", "").unwrap_err(), PrepareDiffError::IncompleteEndpoints);
|
||
for rows in [
|
||
vec![diff_row(DiffRowKind::Removed, Some(1), None, 0)],
|
||
vec![diff_row(DiffRowKind::Added, Some(0), None, 0)],
|
||
vec![diff_row(DiffRowKind::Equal, Some(0), None, 0)],
|
||
vec![diff_row(DiffRowKind::Removed, Some(0), None, 0); 2],
|
||
] {
|
||
assert!(matches!(CodeDocument::prepare_diff(&rows, "x", ""), Err(PrepareDiffError::InvalidRow { .. })));
|
||
}
|
||
assert_eq!(CodeDocument::prepare_diff(&[diff_row(DiffRowKind::Equal, Some(0), Some(0), 0)], "x", "y").unwrap_err(),
|
||
PrepareDiffError::UnequalContent { row: 0 });
|
||
}
|
||
|
||
#[test]
|
||
fn diff_prepared_replacement_retains_runs_diagnostics_and_separate_hunks() {
|
||
let prepared = CodeDocument::prepare_diff(&[
|
||
diff_row(DiffRowKind::Removed, Some(0), None, 4),
|
||
diff_row(DiffRowKind::Removed, Some(1), None, 4),
|
||
diff_row(DiffRowKind::Added, None, Some(0), 4),
|
||
diff_row(DiffRowKind::Equal, Some(2), Some(1), 4),
|
||
], "a\n\nb", "c\nb").unwrap();
|
||
assert_eq!(prepared.decorations().len(), 2);
|
||
assert_eq!(prepared.decorations()[0].start(), pos(0, 0));
|
||
assert_eq!(prepared.decorations()[0].end(), pos(2, 0));
|
||
let mut replacement = prepared.decorations().to_vec();
|
||
replacement.push(Decoration::new(100, pos(3, 0), pos(3, 1), DecorationType::Warning));
|
||
let mut set = DecorationSet::new();
|
||
set.replace_prepared(replacement.clone()).unwrap();
|
||
let retained = set.clone();
|
||
assert_eq!(set.replace_prepared(vec![replacement[1], replacement[0]]),
|
||
Err(PreparedDecorationsError::OverlappingOrUnsorted));
|
||
assert_eq!(set, retained);
|
||
set.add_decoration(Decoration::new(101, pos(3, 0), pos(3, 1), DecorationType::Error));
|
||
assert_eq!(set.len(), 3);
|
||
assert_eq!(set[2].ty, DecorationType::Error);
|
||
let mut document = CodeDocument::from_prepared_diff(prepared);
|
||
let hunks = document.diff_metadata().unwrap().clone();
|
||
document.replace_prepared_decorations(replacement.clone()).unwrap();
|
||
assert_eq!(&*document.decorations(), &replacement);
|
||
assert_eq!(document.diff_metadata(), Some(&hunks));
|
||
assert_eq!(document.version(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn diff_blank_line_backgrounds_record_full_clipped_code_width() {
|
||
let prepared = CodeDocument::prepare_diff(&[
|
||
diff_row(DiffRowKind::Removed, Some(0), None, 1),
|
||
diff_row(DiffRowKind::Added, None, Some(0), 1),
|
||
diff_row(DiffRowKind::Added, None, Some(1), 1),
|
||
], "\n", "\n\n").unwrap();
|
||
let mut session = attach_diff(prepared);
|
||
let (mut cx, mut editor) = editor();
|
||
editor.show_gutter = false;
|
||
editor.word_wrap = false;
|
||
record_editor(&mut cx, &mut editor, &mut session);
|
||
for (vars, row) in [(&editor.draw_diff_removed.draw_vars, 0), (&editor.draw_diff_added.draw_vars, 2)] {
|
||
let rect = editor.diff_row_rect(&session, row).unwrap();
|
||
let mut size = [0.0; 2];
|
||
let mut position = [0.0; 2];
|
||
assert!(vars.get_instance_on_area(&cx, live_id!(rect_size), &mut size));
|
||
assert!(vars.get_instance_on_area(&cx, live_id!(rect_pos), &mut position));
|
||
let mut color = [0.0; 4];
|
||
assert!(vars.get_instance_on_area(&cx, live_id!(color), &mut color));
|
||
assert!((color[3] - 0.14).abs() < 0.001);
|
||
assert_eq!(size, [rect.size.x as f32, rect.size.y as f32]);
|
||
assert_eq!(position, [rect.pos.x as f32, rect.pos.y as f32]);
|
||
assert_eq!(size[0], (480.0 - editor.pad_left_top.x) as f32);
|
||
assert!(size[1] > 0.0);
|
||
}
|
||
assert!(editor.draw_cursor.draw_vars.area().is_valid(&cx));
|
||
let mut blink = [-1.0];
|
||
assert!(editor.draw_cursor.draw_vars.get_instance_on_area(&cx, live_id!(blink), &mut blink));
|
||
assert_eq!(blink, [0.0]);
|
||
assert_eq!(editor.blink_timer.0, 0);
|
||
editor.viewport_rect.pos.x -= 80.0;
|
||
let rect = editor.diff_row_rect(&session, 0).unwrap();
|
||
assert_eq!(rect.pos.x, editor.unscrolled_rect.pos.x + editor.pad_left_top.x);
|
||
assert_eq!(rect.size.x, 480.0 - editor.pad_left_top.x);
|
||
editor.show_gutter = true;
|
||
record_editor(&mut cx, &mut editor, &mut session);
|
||
assert_eq!(editor.gutter_chars, 8);
|
||
assert!(editor.draw_diff_changed.draw_vars.area().is_valid(&cx));
|
||
let diff_width = editor.metrics(&session).gutter_width;
|
||
session.document().set_gutter_mode(GutterMode::Plain);
|
||
assert!(editor.metrics(&session).gutter_width < diff_width);
|
||
assert!(session.document().is_read_only());
|
||
}
|
||
|
||
#[test]
|
||
fn diff_document_and_session_mutations_are_noops_but_removed_row_copy_works() {
|
||
let prepared = CodeDocument::prepare_diff(&[
|
||
diff_row(DiffRowKind::Removed, Some(0), None, 1),
|
||
diff_row(DiffRowKind::Added, None, Some(0), 1),
|
||
], "removed", "target").unwrap();
|
||
let mut session = attach_diff(prepared);
|
||
let document = session.document().clone();
|
||
assert_eq!(document.ensure_editable(), Err(DocumentMutationError::ReadOnlyDiff));
|
||
session.set_selection(pos(0, 0), Affinity::After, SelectionMode::Simple, NewGroup::No);
|
||
session.move_to(pos(0, 7), Affinity::After, NewGroup::No);
|
||
assert_eq!(session.copy(), "removed");
|
||
let selection = session.selections().to_vec();
|
||
let digest = document.digest();
|
||
let tokens = document.layout().tokens.clone();
|
||
let decorations = document.decorations().to_vec();
|
||
session.insert("(".into());
|
||
session.paste("paste".into());
|
||
session.paste_grouped("paste".into(), 42);
|
||
session.enter();
|
||
session.delete();
|
||
session.backspace();
|
||
session.indent();
|
||
session.outdent();
|
||
assert!(!session.undo());
|
||
assert!(!session.redo());
|
||
document.replace("replacement".into());
|
||
edit(&document, Change::Insert(pos(0, 0), "direct".into()));
|
||
document.edit_linewise(SessionId::default(), EditKind::Other, &SelectionSet::new(), |_, _| panic!("diff must refuse before invoking editor"));
|
||
document.force_new_group();
|
||
assert!(!document.undo(SessionId::default(), &SelectionSet::new()));
|
||
assert!(!document.redo(SessionId::default(), &SelectionSet::new()));
|
||
session.handle_changes();
|
||
assert_eq!(&*session.selections(), &selection);
|
||
assert_eq!(document.as_text().to_string(), "removed\ntarget");
|
||
assert_eq!(document.version(), 0);
|
||
assert_eq!(document.digest(), digest);
|
||
assert_eq!(document.layout().tokens, tokens);
|
||
assert_eq!(&*document.decorations(), &decorations);
|
||
}
|
||
|
||
#[test]
|
||
fn diff_admission_twenty_thousand_rows_off_thread() {
|
||
use std::time::Instant;
|
||
fn assert_send<T: Send>() {}
|
||
assert_send::<PreparedDiffDocument>();
|
||
let (prepared, view, prepare_ms) = std::thread::spawn(|| {
|
||
let old = "let old = 1;\n".repeat(10_000);
|
||
let new = "let new = 2;\n".repeat(10_000);
|
||
let rows = (0..10_000).flat_map(|line| [
|
||
diff_row(DiffRowKind::Removed, Some(line), None, line),
|
||
diff_row(DiffRowKind::Added, None, Some(line), line),
|
||
]).collect::<Vec<_>>();
|
||
let start = Instant::now();
|
||
let prepared = CodeDocument::prepare_diff(&rows, &old, &new).unwrap();
|
||
let view = CodeSession::prepare_view(prepared.document(), None);
|
||
let prepare_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||
(prepared, view, prepare_ms)
|
||
}).join().unwrap();
|
||
let row_count = prepared.row_count();
|
||
assert_eq!(row_count, 20_000);
|
||
let text_ptr = prepared.as_text().as_lines().as_ptr();
|
||
let rows_ptr = prepared.metadata().rows.as_ptr();
|
||
let decorations_ptr = prepared.decorations().as_ptr();
|
||
let start = Instant::now();
|
||
let session = CodeSession::from_prepared(CodeDocument::from_prepared_diff(prepared), view).unwrap();
|
||
let attach_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||
println!("record=diff_admission rows={row_count} prepare_ms={prepare_ms:.6} attach_ms={attach_ms:.6}");
|
||
assert!(attach_ms <= 1.0, "diff attachment exceeded 1 ms: {attach_ms}");
|
||
assert_eq!(session.document().as_text().as_lines().as_ptr(), text_ptr);
|
||
assert_eq!(session.document().diff_metadata().unwrap().rows.as_ptr(), rows_ptr);
|
||
assert_eq!(session.document().decorations().as_ptr(), decorations_ptr);
|
||
assert_eq!(session.layout().height(), row_count as f64);
|
||
}
|