input: Rewrap lines without copying them out of the rope (#2783)
## Description
Previously the code copied each line twice, once at
`Rope::from(changed_text.slice(new_range))` and then again at
`line.to_string()`. Now it is doing essentially the same as the rope
iter_lines(), but without copying it into a new rope, and instead of
copying the line it is borrowing it when it can.
This affects all re-wrapping, like opening files, resizing and editing.
| branch | bytes | blocks |
|-------------------------------------|---------------- |------------|
| main | 31,602,003 | 82,470 |
| reduce-wrapper-alloc | 20,632,839 | 8,342 |
## How to Test
I'm using this test with dhat, maybe it's worth committing?
```rust
use gpui::{
AppContext as _, Bounds, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
TestAppContext, VisualTestContext, Window, WindowBounds, WindowOptions, div, px, size,
};
use gpui_component::input::{Editor, EditorState};
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
const FRAMES: usize = 50;
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../story/examples/fixtures");
struct Root(Entity<EditorState>);
impl Render for Root {
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(Editor::new(&self.0))
}
}
fn language_for(ext: &str) -> &'static str {
match ext {
"rs" => "rust",
"md" => "markdown",
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"rb" => "ruby",
"kt" => "kotlin",
"c" => "c",
"go" => "go",
"html" => "html",
"json" => "json",
"sql" => "sql",
"zig" => "zig",
"lua" => "lua",
"php" => "php",
_ => "",
}
}
#[gpui::test]
fn measure_editor_allocations_per_fixture(cx: &mut TestAppContext) {
let mut fixtures: Vec<(String, String, String)> = std::fs::read_dir(FIXTURES)
.expect("fixtures dir")
.filter_map(|entry| {
let path = entry.ok()?.path();
let ext = path.extension()?.to_str()?.to_string();
let language = language_for(&ext);
if language.is_empty() {
return None;
}
let name = path.file_name()?.to_str()?.to_string();
let content = std::fs::read_to_string(&path).ok()?;
Some((name, language.to_string(), content))
})
.collect();
// A synthetic large file (test.rs repeated, ~20k lines)
if let Some(rust) = fixtures.iter().find(|(name, _, _)| name == "test.rs") {
fixtures.push(("test.rs x20".into(), "rust".into(), rust.2.repeat(20)));
}
// Largest first so the headline number is up top.
fixtures.sort_by_key(|(_, _, content)| std::cmp::Reverse(content.len()));
cx.update(gpui_component::init);
println!("== alloc_dhat: per-fixture, load + {FRAMES} draw frames, 800x600, soft wrap ==");
for (name, language, content) in fixtures {
let lines = content.lines().count();
let mut state = None;
let window = cx.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: Default::default(),
size: size(px(800.), px(600.)),
})),
..Default::default()
},
|window, cx| {
let editor = cx.new(|cx| {
EditorState::new(window, cx)
.language(language.clone())
.soft_wrap(true)
});
state = Some(editor.clone());
cx.new(|_| Root(editor))
},
)
.unwrap()
});
let state = state.unwrap();
let mut cx = VisualTestContext::from_window(window.into(), cx);
// Warm up an empty draw so window/theme one-time costs stay out.
cx.update(|window, cx| window.draw(cx).clear(cx));
let _profiler = dhat::Profiler::builder().testing().build();
let before_load = dhat::HeapStats::get();
cx.update(|window, cx| {
state.update(cx, |state, cx| state.set_value(content.clone(), window, cx));
window.draw(cx).clear(cx);
});
let after_load = dhat::HeapStats::get();
let step = (content.len() / FRAMES).max(1);
for frame in 0..FRAMES {
cx.update(|_, cx| {
state.update(cx, |state, cx| {
let offset = (frame * step).min(content.len());
state.set_selected_range(offset..offset, cx);
cx.notify();
});
});
cx.update(|window, cx| window.draw(cx).clear(cx));
}
let after_draws = dhat::HeapStats::get();
println!(
"{name:>12} ({language}, {lines} lines): load {:>10} bytes / {:>6} blocks, {FRAMES} draws {:>10} bytes / {:>6} blocks",
after_load.total_bytes - before_load.total_bytes,
after_load.total_blocks - before_load.total_blocks,
after_draws.total_bytes - after_load.total_bytes,
after_draws.total_blocks - after_load.total_blocks,
);
drop(_profiler);
}
}
```
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific) A
Andreas Johansson committed
16274ece3203f06d73637ce6873bdccae9b8eb12
Parent: 06a5b0e
Committed by GitHub <noreply@github.com>
on 8/20/2026, 10:34:41 AM