Skip to content

Commit 5d9743f

Browse files
feat: further reduce DefaultSkimItem size (#967)
* Initial commit * Cleanup * Prevent some double allocation behavior * User the proper conversion method * Further attempt to avoid alloc * Actually avoid alloc * Cleanup * Remove an unnecessary alloc * Cleanup * Avoid another alloc --------- Co-authored-by: LoricAndre <57358788+LoricAndre@users.noreply.github.com>
1 parent 65b7c86 commit 5d9743f

File tree

3 files changed

+55
-42
lines changed

3 files changed

+55
-42
lines changed

src/helper/item.rs

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use std::borrow::Cow;
2222
#[derive(Debug)]
2323
pub struct DefaultSkimItem {
2424
/// The text that will be shown on screen.
25-
text: String,
25+
text: Box<str>,
2626

2727
/// The index, for use in matching
2828
index: usize,
@@ -37,11 +37,11 @@ pub struct DefaultSkimItemMetadata {
3737
/// The text that will be output when user press `enter`
3838
/// `Some(..)` => the original input is transformed, could not output `text` directly
3939
/// `None` => that it is safe to output `text` directly
40-
orig_text: Option<String>,
40+
orig_text: Option<Box<str>>,
4141

4242
/// The text stripped of all ansi sequences, used for matching
4343
/// Will be Some when ANSI is enabled, None otherwise
44-
stripped_text: Option<String>,
44+
stripped_text: Option<Box<str>>,
4545

4646
/// A mapping of positions from stripped text to original text.
4747
/// Each element is (byte_position, char_position) in the original raw text.
@@ -55,14 +55,15 @@ pub struct DefaultSkimItemMetadata {
5555
impl DefaultSkimItem {
5656
/// Create a new DefaultSkimItem from text
5757
pub fn new(
58-
orig_text: String,
58+
orig_text: &str,
5959
ansi_enabled: bool,
6060
trans_fields: &[FieldRange],
6161
matching_fields: &[FieldRange],
6262
delimiter: &Regex,
6363
index: usize,
6464
) -> Self {
6565
let using_transform_fields = !trans_fields.is_empty();
66+
let contains_ansi = Self::contains_ansi_escape(orig_text);
6667

6768
// transformed | ANSI | output
6869
//------------------------------------------------------
@@ -74,36 +75,37 @@ impl DefaultSkimItem {
7475
// | |
7576
// +- F -> orig | orig
7677

77-
let (mut orig_text, mut text) = match (using_transform_fields, ansi_enabled) {
78+
let (mut orig_text, mut temp_text): (Option<String>, Box<str>) = match (using_transform_fields, ansi_enabled) {
7879
(true, true) => {
7980
let transformed = parse_transform_fields(delimiter, &orig_text, trans_fields);
80-
(Some(orig_text), transformed)
81+
(Some(orig_text.into()), Box::from(transformed))
8182
}
8283
(true, false) => {
8384
let transformed = parse_transform_fields(delimiter, &escape_ansi(&orig_text), trans_fields);
84-
(Some(orig_text), transformed)
85+
(Some(orig_text.into()), Box::from(transformed))
8586
}
86-
(false, true) => (None, orig_text),
87-
(false, false) => (None, escape_ansi(&orig_text)),
87+
(false, true) => (None, Box::from(orig_text)),
88+
(false, false) if contains_ansi => (None, escape_ansi(&orig_text).into()),
89+
(false, false) => (None, Box::from(orig_text)),
8890
};
8991

9092
// Keep track of whether we have null bytes for special handling
91-
let has_null_bytes = text.contains('\0');
93+
let has_null_bytes = temp_text.contains('\0');
9294

9395
// Preserve original text with null bytes for output if needed
9496
if has_null_bytes && orig_text.is_none() {
95-
orig_text = Some(text.clone());
97+
orig_text = Some(temp_text.to_string());
9698
}
9799

98100
// Strip null bytes from text used for display and matching
99101
// Null bytes are control characters that cause rendering issues (zero-width)
100102
// They are preserved in orig_text for output
101103
if has_null_bytes {
102-
text = text.replace('\0', "");
104+
temp_text = temp_text.to_string().replace('\0', "").into_boxed_str();
103105
}
104106

105-
let (stripped_text, ansi_info) = if ansi_enabled {
106-
let (stripped, info) = strip_ansi(&text);
107+
let (stripped_text, ansi_info) = if ansi_enabled && contains_ansi {
108+
let (stripped, info) = strip_ansi(&temp_text);
107109
(Some(stripped), Some(info))
108110
} else {
109111
(None, None)
@@ -116,15 +118,15 @@ impl DefaultSkimItem {
116118
let text_for_matching = if ansi_enabled {
117119
stripped_text.as_ref().unwrap()
118120
} else {
119-
&text
121+
temp_text.as_ref()
120122
};
121123

122124
// Parse the original text with null bytes to determine field boundaries
123125
// Then extract those fields, strip null bytes, and recalculate positions
124126
let orig_text_for_fields = if has_null_bytes {
125-
orig_text.as_ref().unwrap()
127+
orig_text.as_deref().unwrap()
126128
} else {
127-
text_for_matching
129+
&text_for_matching
128130
};
129131

130132
if has_null_bytes {
@@ -134,7 +136,7 @@ impl DefaultSkimItem {
134136

135137
for field in matching_fields {
136138
// Get the field text from original (with null bytes)
137-
if let Some(field_text) = crate::field::get_string_by_field(delimiter, orig_text_for_fields, field)
139+
if let Some(field_text) = crate::field::get_string_by_field(delimiter, &orig_text_for_fields, field)
138140
{
139141
// Strip null bytes from this field
140142
let cleaned_field = field_text.replace('\0', "");
@@ -156,34 +158,43 @@ impl DefaultSkimItem {
156158
let metadata =
157159
if orig_text.is_some() || stripped_text.is_some() || ansi_info.is_some() || matching_ranges.is_some() {
158160
Some(Box::new(DefaultSkimItemMetadata {
159-
orig_text,
160-
stripped_text,
161+
orig_text: orig_text.map(|inner| inner.into_boxed_str()),
162+
stripped_text: stripped_text.map(|inner| inner.into_boxed_str()),
161163
ansi_info,
162164
matching_ranges,
163165
}))
164166
} else {
165167
None
166168
};
167169

168-
DefaultSkimItem { text, index, metadata }
170+
DefaultSkimItem {
171+
text: temp_text,
172+
index,
173+
metadata,
174+
}
175+
}
176+
177+
fn contains_ansi_escape(s: &str) -> bool {
178+
s.contains('\x1b')
169179
}
180+
170181
/// Getter for stripped_text stored in the metadata
171-
pub fn stripped_text(&self) -> Option<&String> {
182+
pub fn stripped_text(&self) -> Option<&str> {
172183
if let Some(meta) = &self.metadata
173184
&& let Some(stripped_text) = &meta.stripped_text
174185
{
175-
Some(stripped_text)
186+
Some(stripped_text.as_ref())
176187
} else {
177188
None
178189
}
179190
}
180191

181192
/// Getter for orig_text stored in metadata
182-
pub fn orig_text(&self) -> Option<&String> {
193+
pub fn orig_text(&self) -> Option<&str> {
183194
if let Some(meta) = &self.metadata
184195
&& let Some(orig) = &meta.orig_text
185196
{
186-
Some(orig)
197+
Some(orig.as_ref())
187198
} else {
188199
None
189200
}
@@ -636,7 +647,7 @@ mod test {
636647
let input = "\x1b[32mgreen\x1b[0m text";
637648
let delimiter = Regex::new(r"\s+").unwrap();
638649
let item = DefaultSkimItem::new(
639-
input.to_string(),
650+
input,
640651
true, // ansi_enabled
641652
&[],
642653
&[],
@@ -678,7 +689,7 @@ mod test {
678689
let input = "😀\x1b[32mtext\x1b[0m";
679690
let delimiter = Regex::new(r"\s+").unwrap();
680691
let item = DefaultSkimItem::new(
681-
input.to_string(),
692+
input,
682693
true, // ansi_enabled
683694
&[],
684695
&[],
@@ -712,7 +723,7 @@ mod test {
712723

713724
// Test with ANSI enabled
714725
let item_ansi = DefaultSkimItem::new(
715-
"\x1b[31mred\x1b[0m".to_string(),
726+
"\x1b[31mred\x1b[0m",
716727
true, // ansi_enabled
717728
&[],
718729
&[],
@@ -727,7 +738,7 @@ mod test {
727738

728739
// Test with ANSI disabled
729740
let item_no_ansi = DefaultSkimItem::new(
730-
"\x1b[31mred\x1b[0m".to_string(),
741+
"\x1b[31mred\x1b[0m",
731742
false, // ansi_enabled
732743
&[],
733744
&[],
@@ -751,7 +762,7 @@ mod test {
751762

752763
// Create item with ANSI codes: "\x1b[32mgreen\x1b[0m"
753764
let item = DefaultSkimItem::new(
754-
"\x1b[32mgreen\x1b[0m".to_string(),
765+
"\x1b[32mgreen\x1b[0m",
755766
true, // ansi_enabled
756767
&[],
757768
&[],
@@ -790,7 +801,7 @@ mod test {
790801

791802
// Create item with ANSI codes: "\x1b[32mgreen\x1b[0m"
792803
let item = DefaultSkimItem::new(
793-
"\x1b[32mgreen\x1b[0m".to_string(),
804+
"\x1b[32mgreen\x1b[0m",
794805
true, // ansi_enabled
795806
&[],
796807
&[],
@@ -831,7 +842,7 @@ mod test {
831842

832843
// Create item with ANSI codes: "\x1b[32mgreen\x1b[0m"
833844
let item = DefaultSkimItem::new(
834-
"\x1b[32mgreen\x1b[0m".to_string(),
845+
"\x1b[32mgreen\x1b[0m",
835846
true, // ansi_enabled
836847
&[],
837848
&[],
@@ -871,7 +882,7 @@ mod test {
871882

872883
// Create item with ANSI codes: "\x1b[32mgreen_text\x1b[0m"
873884
let item = DefaultSkimItem::new(
874-
"\x1b[32mgreen_text\x1b[0m".to_string(),
885+
"\x1b[32mgreen_text\x1b[0m",
875886
true, // ansi_enabled
876887
&[],
877888
&[], // no matching fields restriction
@@ -903,7 +914,7 @@ mod test {
903914

904915
// Create item with matching field 2
905916
let item = DefaultSkimItem::new(
906-
text.to_string(),
917+
text,
907918
false, // no ansi
908919
&[], // no transform fields
909920
&[FieldRange::Single(2)], // match field 2

src/helper/item_reader.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,14 @@ impl SkimItemReader {
223223
buffer.pop();
224224
}
225225

226-
let line = String::from_utf8_lossy(&buffer).to_string();
226+
let Ok(line) = std::str::from_utf8(&buffer) else {
227+
continue;
228+
};
227229

228-
trace!("got item {} with index {}", line.clone(), line_idx);
230+
trace!("got item {} with index {}", line, line_idx);
229231

230232
let raw_item = DefaultSkimItem::new(
231-
line,
233+
&line,
232234
option.use_ansi_color,
233235
&transform_fields,
234236
&matching_fields,
@@ -325,7 +327,7 @@ impl SkimItemReader {
325327
.lines()
326328
.map(|line| {
327329
Arc::new(DefaultSkimItem::new(
328-
line.to_string(),
330+
line,
329331
false,
330332
&[],
331333
&[],

tests/common/insta.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ impl TestHarness {
179179
pub fn add_items<I, S>(&mut self, items: I) -> Result<()>
180180
where
181181
I: IntoIterator<Item = S>,
182-
S: Into<String>,
182+
S: AsRef<str>,
183183
{
184184
// Parse field ranges from options
185185
let transform_fields: Vec<FieldRange> = self
@@ -203,7 +203,7 @@ impl TestHarness {
203203
.enumerate()
204204
.map(|(idx, s)| {
205205
Arc::new(DefaultSkimItem::new(
206-
s.into(),
206+
s.as_ref(),
207207
self.app.options.ansi,
208208
&transform_fields,
209209
&matching_fields,
@@ -257,7 +257,7 @@ impl TestHarness {
257257
.enumerate()
258258
.map(|(idx, s)| {
259259
Arc::new(DefaultSkimItem::new(
260-
s,
260+
&s,
261261
self.app.options.ansi,
262262
&transform_fields,
263263
&matching_fields,
@@ -430,7 +430,7 @@ pub fn enter_default() -> Result<TestHarness> {
430430
pub fn enter_items<I, S>(items: I, options: SkimOptions) -> Result<TestHarness>
431431
where
432432
I: IntoIterator<Item = S>,
433-
S: Into<String>,
433+
S: AsRef<str>,
434434
{
435435
let mut harness = enter(options)?;
436436
harness.add_items(items)?;

0 commit comments

Comments
 (0)