use super::*; use crate::LanguageConfig; use rand::rngs::StdRng; use std::{env, ops::Range, sync::Arc}; use text::Buffer; use tree_sitter::Node; use unindent::Unindent as _; use util::test::marked_text_ranges; #[test] fn test_splice_included_ranges() { let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)]; let new_ranges = splice_included_ranges( ranges.clone(), &[54..56, 58..68], &[ts_range(50..54), ts_range(59..67)], ); assert_eq!( new_ranges, &[ ts_range(20..30), ts_range(50..54), ts_range(59..67), ts_range(80..90), ] ); let new_ranges = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]); assert_eq!( new_ranges, &[ts_range(20..30), ts_range(50..60), ts_range(80..90)] ); let new_ranges = splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]); assert_eq!( new_ranges, &[ ts_range(0..2), ts_range(20..30), ts_range(50..60), ts_range(70..75), ts_range(80..90) ] ); let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]); assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]); fn ts_range(range: Range) -> tree_sitter::Range { tree_sitter::Range { start_byte: range.start, start_point: tree_sitter::Point { row: 0, column: range.start, }, end_byte: range.end, end_point: tree_sitter::Point { row: 0, column: range.end, }, } } } #[gpui::test] fn test_syntax_map_layers_for_range() { let registry = Arc::new(LanguageRegistry::test()); let language = Arc::new(rust_lang()); registry.add(language.clone()); let mut buffer = Buffer::new( 0, 0, r#" fn a() { assert_eq!( b(vec![C {}]), vec![d.e], ); println!("{}", f(|_| true)); } "# .unindent(), ); let mut syntax_map = SyntaxMap::new(); syntax_map.set_language_registry(registry.clone()); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(2, 0)..Point::new(2, 0), &[ "...(function_item ... (block (expression_statement (macro_invocation...", "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...", ], ); assert_layers_for_range( &syntax_map, &buffer, Point::new(2, 14)..Point::new(2, 16), &[ "...(function_item ...", "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...", "...(array_expression (struct_expression ...", ], ); assert_layers_for_range( &syntax_map, &buffer, Point::new(3, 14)..Point::new(3, 16), &[ "...(function_item ...", "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...", "...(array_expression (field_expression ...", ], ); assert_layers_for_range( &syntax_map, &buffer, Point::new(5, 12)..Point::new(5, 16), &[ "...(function_item ...", "...(call_expression ... (arguments (closure_expression ...", ], ); // Replace a vec! macro invocation with a plain slice, removing a syntactic layer. let macro_name_range = range_for_text(&buffer, "vec!"); buffer.edit([(macro_name_range, "&")]); syntax_map.interpolate(&buffer); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(2, 14)..Point::new(2, 16), &[ "...(function_item ...", "...(tuple_expression (call_expression ... arguments: (arguments (reference_expression value: (array_expression...", ], ); // Put the vec! macro back, adding back the syntactic layer. buffer.undo(); syntax_map.interpolate(&buffer); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(2, 14)..Point::new(2, 16), &[ "...(function_item ...", "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...", "...(array_expression (struct_expression ...", ], ); } #[gpui::test] fn test_dynamic_language_injection() { let registry = Arc::new(LanguageRegistry::test()); let markdown = Arc::new(markdown_lang()); registry.add(markdown.clone()); registry.add(Arc::new(rust_lang())); registry.add(Arc::new(ruby_lang())); let mut buffer = Buffer::new( 0, 0, r#" This is a code block: ```rs fn foo() {} ``` "# .unindent(), ); let mut syntax_map = SyntaxMap::new(); syntax_map.set_language_registry(registry.clone()); syntax_map.reparse(markdown.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(3, 0)..Point::new(3, 0), &[ "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", "...(function_item name: (identifier) parameters: (parameters) body: (block)...", ], ); // Replace Rust with Ruby in code block. let macro_name_range = range_for_text(&buffer, "rs"); buffer.edit([(macro_name_range, "ruby")]); syntax_map.interpolate(&buffer); syntax_map.reparse(markdown.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(3, 0)..Point::new(3, 0), &[ "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", "...(call method: (identifier) arguments: (argument_list (call method: (identifier) arguments: (argument_list) block: (block)...", ], ); // Replace Ruby with a language that hasn't been loaded yet. let macro_name_range = range_for_text(&buffer, "ruby"); buffer.edit([(macro_name_range, "html")]); syntax_map.interpolate(&buffer); syntax_map.reparse(markdown.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(3, 0)..Point::new(3, 0), &[ "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter..." ], ); assert!(syntax_map.contains_unknown_injections()); registry.add(Arc::new(html_lang())); syntax_map.reparse(markdown.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, Point::new(3, 0)..Point::new(3, 0), &[ "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", "(fragment (text))", ], ); assert!(!syntax_map.contains_unknown_injections()); } #[gpui::test] fn test_typing_multiple_new_injections() { let (buffer, syntax_map) = test_edit_sequence( "Rust", &[ "fn a() { dbg }", "fn a() { dbg«!» }", "fn a() { dbg!«()» }", "fn a() { dbg!(«b») }", "fn a() { dbg!(b«.») }", "fn a() { dbg!(b.«c») }", "fn a() { dbg!(b.c«()») }", "fn a() { dbg!(b.c(«vec»)) }", "fn a() { dbg!(b.c(vec«!»)) }", "fn a() { dbg!(b.c(vec!«[]»)) }", "fn a() { dbg!(b.c(vec![«d»])) }", "fn a() { dbg!(b.c(vec![d«.»])) }", "fn a() { dbg!(b.c(vec![d.«e»])) }", ], ); assert_capture_ranges( &syntax_map, &buffer, &["field"], "fn a() { dbg!(b.«c»(vec![d.«e»])) }", ); } #[gpui::test] fn test_pasting_new_injection_line_between_others() { let (buffer, syntax_map) = test_edit_sequence( "Rust", &[ " fn a() { b!(B {}); c!(C {}); d!(D {}); e!(E {}); f!(F {}); g!(G {}); } ", " fn a() { b!(B {}); c!(C {}); d!(D {}); « h!(H {}); » e!(E {}); f!(F {}); g!(G {}); } ", ], ); assert_capture_ranges( &syntax_map, &buffer, &["struct"], " fn a() { b!(«B {}»); c!(«C {}»); d!(«D {}»); h!(«H {}»); e!(«E {}»); f!(«F {}»); g!(«G {}»); } ", ); } #[gpui::test] fn test_joining_injections_with_child_injections() { let (buffer, syntax_map) = test_edit_sequence( "Rust", &[ " fn a() { b!( c![one.two.three], d![four.five.six], ); e!( f![seven.eight], ); } ", " fn a() { b!( c![one.two.three], d![four.five.six], ˇ f![seven.eight], ); } ", ], ); assert_capture_ranges( &syntax_map, &buffer, &["field"], " fn a() { b!( c![one.«two».«three»], d![four.«five».«six»], f![seven.«eight»], ); } ", ); } #[gpui::test] fn test_editing_edges_of_injection() { test_edit_sequence( "Rust", &[ " fn a() { b!(c!()) } ", " fn a() { «d»!(c!()) } ", " fn a() { «e»d!(c!()) } ", " fn a() { ed!«[»c!()«]» } ", ], ); } #[gpui::test] fn test_edits_preceding_and_intersecting_injection() { test_edit_sequence( "Rust", &[ // "const aaaaaaaaaaaa: B = c!(d(e.f));", "const aˇa: B = c!(d(eˇ));", ], ); } #[gpui::test] fn test_non_local_changes_create_injections() { test_edit_sequence( "Rust", &[ " // a! { static B: C = d; // } ", " ˇa! { static B: C = d; ˇ} ", ], ); } #[gpui::test] fn test_creating_many_injections_in_one_edit() { test_edit_sequence( "Rust", &[ " fn a() { one(Two::three(3)); four(Five::six(6)); seven(Eight::nine(9)); } ", " fn a() { one«!»(Two::three(3)); four«!»(Five::six(6)); seven«!»(Eight::nine(9)); } ", " fn a() { one!(Two::three«!»(3)); four!(Five::six«!»(6)); seven!(Eight::nine«!»(9)); } ", ], ); } #[gpui::test] fn test_editing_across_injection_boundary() { test_edit_sequence( "Rust", &[ " fn one() { two(); three!( three.four, five.six, ); } ", " fn one() { two(); th«irty_five![» three.four, five.six, « seven.eight, ];» } ", ], ); } #[gpui::test] fn test_removing_injection_by_replacing_across_boundary() { test_edit_sequence( "Rust", &[ " fn one() { two!( three.four, ); } ", " fn one() { t«en .eleven( twelve, » three.four, ); } ", ], ); } #[gpui::test] fn test_combined_injections() { let (buffer, syntax_map) = test_edit_sequence( "ERB", &[ " <% if @one %>
<% else %>
<% end %>
", " <% if @one %>
ˇ else ˇ
<% end %>
", " <% if @one «;» end %>
", ], ); assert_capture_ranges( &syntax_map, &buffer, &["tag", "ivar"], " <«body»> <% if «@one» ; end %> ", ); } #[gpui::test] fn test_combined_injections_empty_ranges() { test_edit_sequence( "ERB", &[ " <% if @one %> <% else %> <% end %> ", " <% if @one %> ˇ<% end %> ", ], ); } #[gpui::test] fn test_combined_injections_edit_edges_of_ranges() { let (buffer, syntax_map) = test_edit_sequence( "ERB", &[ " <%= one @two %> <%= three @four %> ", " <%= one @two %ˇ <%= three @four %> ", " <%= one @two %«>» <%= three @four %> ", ], ); assert_capture_ranges( &syntax_map, &buffer, &["tag", "ivar"], " <%= one «@two» %> <%= three «@four» %> ", ); } #[gpui::test] fn test_combined_injections_splitting_some_injections() { let (_buffer, _syntax_map) = test_edit_sequence( "ERB", &[ r#" <%A if b(:c) %> d <% end %> eee <% f %> "#, r#" <%« AAAAAAA %> hhhhhhh <%=» if b(:c) %> d <% end %> eee <% f %> "#, ], ); } #[gpui::test] fn test_combined_injections_inside_injections() { let (_buffer, _syntax_map) = test_edit_sequence( "Markdown", &[ r#" here is some ERB code: ```erb ``` "#, r#" here is some ERB code: ```erb ``` "#, ], ); } #[gpui::test(iterations = 50)] fn test_random_syntax_map_edits(mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); let text = r#" fn test_something() { let vec = vec![5, 1, 3, 8]; assert_eq!( vec .into_iter() .map(|i| i * 2) .collect::>(), vec![ 5 * 2, 1 * 2, 3 * 2, 8 * 2 ], ); } "# .unindent() .repeat(2); let registry = Arc::new(LanguageRegistry::test()); let language = Arc::new(rust_lang()); registry.add(language.clone()); let mut buffer = Buffer::new(0, 0, text); let mut syntax_map = SyntaxMap::new(); syntax_map.set_language_registry(registry.clone()); syntax_map.reparse(language.clone(), &buffer); let mut reference_syntax_map = SyntaxMap::new(); reference_syntax_map.set_language_registry(registry.clone()); log::info!("initial text:\n{}", buffer.text()); for _ in 0..operations { let prev_buffer = buffer.snapshot(); let prev_syntax_map = syntax_map.snapshot(); buffer.randomly_edit(&mut rng, 3); log::info!("text:\n{}", buffer.text()); syntax_map.interpolate(&buffer); check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer); syntax_map.reparse(language.clone(), &buffer); reference_syntax_map.clear(); reference_syntax_map.reparse(language.clone(), &buffer); } for i in 0..operations { let i = operations - i - 1; buffer.undo(); log::info!("undoing operation {}", i); log::info!("text:\n{}", buffer.text()); syntax_map.interpolate(&buffer); syntax_map.reparse(language.clone(), &buffer); reference_syntax_map.clear(); reference_syntax_map.reparse(language.clone(), &buffer); assert_eq!( syntax_map.layers(&buffer).len(), reference_syntax_map.layers(&buffer).len(), "wrong number of layers after undoing edit {i}" ); } let layers = syntax_map.layers(&buffer); let reference_layers = reference_syntax_map.layers(&buffer); for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) { assert_eq!( edited_layer.node().to_sexp(), reference_layer.node().to_sexp() ); assert_eq!(edited_layer.node().range(), reference_layer.node().range()); } } #[gpui::test(iterations = 50)] fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); let text = r#"
<% if one?(:two) %>

<%= yield :five %>

<% elsif Six.seven(8) %>

<%= yield :five %>

<% else %> Ok <% end %>
"# .unindent() .repeat(8); let registry = Arc::new(LanguageRegistry::test()); let language = Arc::new(erb_lang()); registry.add(language.clone()); registry.add(Arc::new(ruby_lang())); registry.add(Arc::new(html_lang())); let mut buffer = Buffer::new(0, 0, text); let mut syntax_map = SyntaxMap::new(); syntax_map.set_language_registry(registry.clone()); syntax_map.reparse(language.clone(), &buffer); let mut reference_syntax_map = SyntaxMap::new(); reference_syntax_map.set_language_registry(registry.clone()); log::info!("initial text:\n{}", buffer.text()); for _ in 0..operations { let prev_buffer = buffer.snapshot(); let prev_syntax_map = syntax_map.snapshot(); buffer.randomly_edit(&mut rng, 3); log::info!("text:\n{}", buffer.text()); syntax_map.interpolate(&buffer); check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer); syntax_map.reparse(language.clone(), &buffer); reference_syntax_map.clear(); reference_syntax_map.reparse(language.clone(), &buffer); } for i in 0..operations { let i = operations - i - 1; buffer.undo(); log::info!("undoing operation {}", i); log::info!("text:\n{}", buffer.text()); syntax_map.interpolate(&buffer); syntax_map.reparse(language.clone(), &buffer); reference_syntax_map.clear(); reference_syntax_map.reparse(language.clone(), &buffer); assert_eq!( syntax_map.layers(&buffer).len(), reference_syntax_map.layers(&buffer).len(), "wrong number of layers after undoing edit {i}" ); } let layers = syntax_map.layers(&buffer); let reference_layers = reference_syntax_map.layers(&buffer); for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) { assert_eq!( edited_layer.node().to_sexp(), reference_layer.node().to_sexp() ); assert_eq!(edited_layer.node().range(), reference_layer.node().range()); } } fn check_interpolation( old_syntax_map: &SyntaxSnapshot, new_syntax_map: &SyntaxSnapshot, old_buffer: &BufferSnapshot, new_buffer: &BufferSnapshot, ) { let edits = new_buffer .edits_since::(&old_buffer.version()) .collect::>(); for (old_layer, new_layer) in old_syntax_map .layers .iter() .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); let Some(old_tree) = old_layer.content.tree() else { continue }; let Some(new_tree) = new_layer.content.tree() else { continue }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); let new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point(); let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point); let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point); check_node_edits( old_layer.depth, &old_layer.range, old_node, new_node, old_buffer, new_buffer, &edits, ); } fn check_node_edits( depth: usize, range: &Range, old_node: Node, new_node: Node, old_buffer: &BufferSnapshot, new_buffer: &BufferSnapshot, edits: &[text::Edit], ) { assert_eq!(old_node.kind(), new_node.kind()); let old_range = old_node.byte_range(); let new_range = new_node.byte_range(); let is_edited = edits .iter() .any(|edit| edit.new.start < new_range.end && edit.new.end > new_range.start); if is_edited { assert!( new_node.has_changes(), concat!( "failed to mark node as edited.\n", "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n", "node kind: {}, old node range: {:?}, new node range: {:?}", ), depth, range.to_offset(old_buffer), range.to_offset(new_buffer), new_node.kind(), old_range, new_range, ); } if !new_node.has_changes() { assert_eq!( old_buffer .text_for_range(old_range.clone()) .collect::(), new_buffer .text_for_range(new_range.clone()) .collect::(), concat!( "mismatched text for node\n", "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n", "node kind: {}, old node range:{:?}, new node range:{:?}", ), depth, range.to_offset(old_buffer), range.to_offset(new_buffer), new_node.kind(), old_range, new_range, ); } for i in 0..new_node.child_count() { check_node_edits( depth, range, old_node.child(i).unwrap(), new_node.child(i).unwrap(), old_buffer, new_buffer, edits, ) } } } fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) { let registry = Arc::new(LanguageRegistry::test()); registry.add(Arc::new(rust_lang())); registry.add(Arc::new(ruby_lang())); registry.add(Arc::new(html_lang())); registry.add(Arc::new(erb_lang())); registry.add(Arc::new(markdown_lang())); let language = registry .language_for_name(language_name) .now_or_never() .unwrap() .unwrap(); let mut buffer = Buffer::new(0, 0, Default::default()); let mut mutated_syntax_map = SyntaxMap::new(); mutated_syntax_map.set_language_registry(registry.clone()); mutated_syntax_map.reparse(language.clone(), &buffer); for (i, marked_string) in steps.into_iter().enumerate() { buffer.edit_via_marked_text(&marked_string.unindent()); // Reparse the syntax map mutated_syntax_map.interpolate(&buffer); mutated_syntax_map.reparse(language.clone(), &buffer); // Create a second syntax map from scratch let mut reference_syntax_map = SyntaxMap::new(); reference_syntax_map.set_language_registry(registry.clone()); reference_syntax_map.reparse(language.clone(), &buffer); // Compare the mutated syntax map to the new syntax map let mutated_layers = mutated_syntax_map.layers(&buffer); let reference_layers = reference_syntax_map.layers(&buffer); assert_eq!( mutated_layers.len(), reference_layers.len(), "wrong number of layers at step {i}" ); for (edited_layer, reference_layer) in mutated_layers.into_iter().zip(reference_layers.into_iter()) { assert_eq!( edited_layer.node().to_sexp(), reference_layer.node().to_sexp(), "different layer at step {i}" ); assert_eq!( edited_layer.node().range(), reference_layer.node().range(), "different layer at step {i}" ); } } (buffer, mutated_syntax_map) } fn html_lang() -> Language { Language::new( LanguageConfig { name: "HTML".into(), path_suffixes: vec!["html".to_string()], ..Default::default() }, Some(tree_sitter_html::language()), ) .with_highlights_query( r#" (tag_name) @tag (erroneous_end_tag_name) @tag (attribute_name) @property "#, ) .unwrap() } fn ruby_lang() -> Language { Language::new( LanguageConfig { name: "Ruby".into(), path_suffixes: vec!["rb".to_string()], ..Default::default() }, Some(tree_sitter_ruby::language()), ) .with_highlights_query( r#" ["if" "do" "else" "end"] @keyword (instance_variable) @ivar "#, ) .unwrap() } fn erb_lang() -> Language { Language::new( LanguageConfig { name: "ERB".into(), path_suffixes: vec!["erb".to_string()], ..Default::default() }, Some(tree_sitter_embedded_template::language()), ) .with_highlights_query( r#" ["<%" "%>"] @keyword "#, ) .unwrap() .with_injection_query( r#" ( (code) @content (#set! "language" "ruby") (#set! "combined") ) ( (content) @content (#set! "language" "html") (#set! "combined") ) "#, ) .unwrap() } fn rust_lang() -> Language { Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], ..Default::default() }, Some(tree_sitter_rust::language()), ) .with_highlights_query( r#" (field_identifier) @field (struct_expression) @struct "#, ) .unwrap() .with_injection_query( r#" (macro_invocation (token_tree) @content (#set! "language" "rust")) "#, ) .unwrap() } fn markdown_lang() -> Language { Language::new( LanguageConfig { name: "Markdown".into(), path_suffixes: vec!["md".into()], ..Default::default() }, Some(tree_sitter_markdown::language()), ) .with_injection_query( r#" (fenced_code_block (info_string (language) @language) (code_fence_content) @content) "#, ) .unwrap() } fn range_for_text(buffer: &Buffer, text: &str) -> Range { let start = buffer.as_rope().to_string().find(text).unwrap(); start..start + text.len() } fn assert_layers_for_range( syntax_map: &SyntaxMap, buffer: &BufferSnapshot, range: Range, expected_layers: &[&str], ) { let layers = syntax_map .layers_for_range(range, &buffer) .collect::>(); assert_eq!( layers.len(), expected_layers.len(), "wrong number of layers" ); for (i, (layer, expected_s_exp)) in layers.iter().zip(expected_layers.iter()).enumerate() { let actual_s_exp = layer.node().to_sexp(); assert!( string_contains_sequence( &actual_s_exp, &expected_s_exp.split("...").collect::>() ), "layer {i}:\n\nexpected: {expected_s_exp}\nactual: {actual_s_exp}", ); } } fn assert_capture_ranges( syntax_map: &SyntaxMap, buffer: &BufferSnapshot, highlight_query_capture_names: &[&str], marked_string: &str, ) { let mut actual_ranges = Vec::>::new(); let captures = syntax_map.captures(0..buffer.len(), buffer, |grammar| { grammar.highlights_query.as_ref() }); let queries = captures .grammars() .iter() .map(|grammar| grammar.highlights_query.as_ref().unwrap()) .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; if highlight_query_capture_names.contains(&name.as_str()) { actual_ranges.push(capture.node.byte_range()); } } let (text, expected_ranges) = marked_text_ranges(&marked_string.unindent(), false); assert_eq!(text, buffer.text()); assert_eq!(actual_ranges, expected_ranges); } pub fn string_contains_sequence(text: &str, parts: &[&str]) -> bool { let mut last_part_end = 0; for part in parts { if let Some(start_ix) = text[last_part_end..].find(part) { last_part_end = start_ix + part.len(); } else { return false; } } true }