languages: Correctly calculate ranges in `label_for_completion` (#44925)

Nereuxofficial and Kirill Bulatov created

Closes #44825

Release Notes:

- Fixed a case where an incorrect match could be generated in
label_for_completion

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Change summary

crates/language/src/language.rs |  5 ++
crates/languages/src/rust.rs    | 48 ++++++++++++++++++++++++++++++++--
2 files changed, 49 insertions(+), 4 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -2425,7 +2425,10 @@ impl CodeLabel {
             "invalid filter range"
         );
         runs.iter().for_each(|(range, _)| {
-            assert!(text.get(range.clone()).is_some(), "invalid run range");
+            assert!(
+                text.get(range.clone()).is_some(),
+                "invalid run range with inputs. Requested range {range:?} in text '{text}'",
+            );
         });
         Self {
             runs,

crates/languages/src/rust.rs 🔗

@@ -375,16 +375,20 @@ impl LspAdapter for RustLspAdapter {
                         let start_pos = range.start as usize;
                         let end_pos = range.end as usize;
 
-                        label.push_str(&snippet.text[text_pos..end_pos]);
-                        text_pos = end_pos;
+                        label.push_str(&snippet.text[text_pos..start_pos]);
 
                         if start_pos == end_pos {
                             let caret_start = label.len();
                             label.push('…');
                             runs.push((caret_start..label.len(), HighlightId::TABSTOP_INSERT_ID));
                         } else {
-                            runs.push((start_pos..end_pos, HighlightId::TABSTOP_REPLACE_ID));
+                            let label_start = label.len();
+                            label.push_str(&snippet.text[start_pos..end_pos]);
+                            let label_end = label.len();
+                            runs.push((label_start..label_end, HighlightId::TABSTOP_REPLACE_ID));
                         }
+
+                        text_pos = end_pos;
                     }
 
                     label.push_str(&snippet.text[text_pos..]);
@@ -1592,6 +1596,44 @@ mod tests {
                 ],
             ))
         );
+
+        // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
+        let res = adapter
+            .label_for_completion(
+                &lsp::CompletionItem {
+                    kind: Some(lsp::CompletionItemKind::STRUCT),
+                    label: "Particles".to_string(),
+                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        range: lsp::Range::default(),
+                        new_text: "Particles { pos_x: $1, pos_y: $2, vel_x: $3, vel_y: $4, acc_x: ${5:()}, acc_y: ${6:()}, mass: $7 }$0".to_string(),
+                    })),
+                    ..Default::default()
+                },
+                &language,
+            )
+            .await
+            .unwrap();
+
+        assert_eq!(
+            res,
+            CodeLabel::new(
+                "Particles { pos_x: …, pos_y: …, vel_x: …, vel_y: …, acc_x: (), acc_y: (), mass: … }".to_string(),
+                0..9,
+                vec![
+                    (19..22, HighlightId::TABSTOP_INSERT_ID),
+                    (31..34, HighlightId::TABSTOP_INSERT_ID),
+                    (43..46, HighlightId::TABSTOP_INSERT_ID),
+                    (55..58, HighlightId::TABSTOP_INSERT_ID),
+                    (67..69, HighlightId::TABSTOP_REPLACE_ID),
+                    (78..80, HighlightId::TABSTOP_REPLACE_ID),
+                    (88..91, HighlightId::TABSTOP_INSERT_ID),
+                    (0..9, highlight_type),
+                    (60..65, highlight_field),
+                    (71..76, highlight_field),
+                ],
+            )
+        );
     }
 
     #[gpui::test]