@@ -17,6 +17,8 @@ use tower_lsp_server::lsp_types::CompletionItemKind;
1717use tower_lsp_server:: lsp_types:: Documentation ;
1818use tower_lsp_server:: lsp_types:: InsertTextFormat ;
1919use tower_lsp_server:: lsp_types:: Position ;
20+ use tower_lsp_server:: lsp_types:: Range ;
21+ use tower_lsp_server:: lsp_types:: TextEdit ;
2022
2123/// Tracks what closing characters are needed to complete a template tag.
2224///
@@ -119,7 +121,15 @@ pub fn handle_completion(
119121 } ;
120122
121123 // Generate completions based on available template tags
122- generate_template_completions ( & context, template_tags, tag_specs, supports_snippets)
124+ generate_template_completions (
125+ & context,
126+ template_tags,
127+ tag_specs,
128+ supports_snippets,
129+ position,
130+ & line_info. text ,
131+ line_info. cursor_offset ,
132+ )
123133}
124134
125135/// Extract line information from document at given position
@@ -280,6 +290,9 @@ fn generate_template_completions(
280290 template_tags : Option < & TemplateTags > ,
281291 tag_specs : Option < & TagSpecs > ,
282292 supports_snippets : bool ,
293+ position : Position ,
294+ line_text : & str ,
295+ cursor_offset : usize ,
283296) -> Vec < CompletionItem > {
284297 match context {
285298 TemplateCompletionContext :: TagName {
@@ -293,6 +306,9 @@ fn generate_template_completions(
293306 template_tags,
294307 tag_specs,
295308 supports_snippets,
309+ position,
310+ line_text,
311+ cursor_offset,
296312 ) ,
297313 TemplateCompletionContext :: TagArgument {
298314 tag,
@@ -322,21 +338,57 @@ fn generate_template_completions(
322338 }
323339}
324340
341+ /// Calculate the range to replace for a completion
342+ fn calculate_replacement_range (
343+ position : Position ,
344+ line_text : & str ,
345+ cursor_offset : usize ,
346+ partial_len : usize ,
347+ closing : & ClosingBrace ,
348+ ) -> Range {
349+ // Start position: move back by the length of the partial text
350+ let start_col = position
351+ . character
352+ . saturating_sub ( u32:: try_from ( partial_len) . unwrap_or ( 0 ) ) ;
353+ let start = Position :: new ( position. line , start_col) ;
354+
355+ // End position: include auto-paired } if present
356+ let mut end_col = position. character ;
357+ if matches ! ( closing, ClosingBrace :: PartialClose ) {
358+ // Include the auto-paired } in the replacement range
359+ // Check if there's a } immediately after cursor
360+ if line_text. len ( ) > cursor_offset && & line_text[ cursor_offset..=cursor_offset] == "}" {
361+ end_col += 1 ;
362+ }
363+ }
364+ let end = Position :: new ( position. line , end_col) ;
365+
366+ Range :: new ( start, end)
367+ }
368+
325369/// Generate completions for tag names
370+ #[ allow( clippy:: too_many_arguments) ]
326371fn generate_tag_name_completions (
327372 partial : & str ,
328373 needs_space : bool ,
329374 closing : & ClosingBrace ,
330375 template_tags : Option < & TemplateTags > ,
331376 tag_specs : Option < & TagSpecs > ,
332377 supports_snippets : bool ,
378+ position : Position ,
379+ line_text : & str ,
380+ cursor_offset : usize ,
333381) -> Vec < CompletionItem > {
334382 let Some ( tags) = template_tags else {
335383 return Vec :: new ( ) ;
336384 } ;
337385
338386 let mut completions = Vec :: new ( ) ;
339387
388+ // Calculate the replacement range for all completions
389+ let replacement_range =
390+ calculate_replacement_range ( position, line_text, cursor_offset, partial. len ( ) , closing) ;
391+
340392 // First, check if we should suggest end tags
341393 // If partial starts with "end", prioritize end tags
342394 if partial. starts_with ( "end" ) && tag_specs. is_some ( ) {
@@ -364,7 +416,9 @@ fn generate_tag_name_completions(
364416 label : end_tag. name . clone ( ) ,
365417 kind : Some ( CompletionItemKind :: KEYWORD ) ,
366418 detail : Some ( format ! ( "End tag for {opener_name}" ) ) ,
367- insert_text : Some ( insert_text) ,
419+ text_edit : Some ( tower_lsp_server:: lsp_types:: CompletionTextEdit :: Edit (
420+ TextEdit :: new ( replacement_range, insert_text. clone ( ) ) ,
421+ ) ) ,
368422 insert_text_format : Some ( InsertTextFormat :: PLAIN_TEXT ) ,
369423 filter_text : Some ( end_tag. name . clone ( ) ) ,
370424 sort_text : Some ( format ! ( "0_{}" , end_tag. name) ) , // Priority sort
@@ -393,14 +447,19 @@ fn generate_tag_name_completions(
393447 text. push ( ' ' ) ;
394448 }
395449
396- // Add tag name and snippet arguments (including end tag if required)
397- text. push_str ( & generate_snippet_for_tag_with_end ( tag. name ( ) , spec) ) ;
398-
399- // Add closing based on what's already present
400- match closing {
401- ClosingBrace :: None => text. push_str ( " %}" ) ,
402- ClosingBrace :: PartialClose => text. push_str ( " %" ) ,
403- ClosingBrace :: FullClose => { } // No closing needed
450+ // Generate the snippet
451+ let snippet = generate_snippet_for_tag_with_end ( tag. name ( ) , spec) ;
452+ text. push_str ( & snippet) ;
453+
454+ // Only add closing if the snippet doesn't already include it
455+ // (snippets for tags with end tags include their own %} closing)
456+ if !snippet. contains ( "%}" ) {
457+ // Add closing based on what's already present
458+ match closing {
459+ ClosingBrace :: None => text. push_str ( " %}" ) ,
460+ ClosingBrace :: PartialClose => text. push_str ( " %" ) ,
461+ ClosingBrace :: FullClose => { } // No closing needed
462+ }
404463 }
405464
406465 ( text, InsertTextFormat :: SNIPPET )
@@ -419,19 +478,21 @@ fn generate_tag_name_completions(
419478 } ;
420479
421480 // Create completion item
422- // Use SNIPPET kind when we're inserting a snippet, FUNCTION otherwise
481+ // Use SNIPPET kind when we're inserting a snippet, KEYWORD otherwise
423482 let kind = if matches ! ( insert_format, InsertTextFormat :: SNIPPET ) {
424483 CompletionItemKind :: SNIPPET
425484 } else {
426- CompletionItemKind :: FUNCTION
485+ CompletionItemKind :: KEYWORD
427486 } ;
428487
429488 let completion_item = CompletionItem {
430489 label : tag. name ( ) . clone ( ) ,
431490 kind : Some ( kind) ,
432491 detail : Some ( format ! ( "from {}" , tag. library( ) ) ) ,
433492 documentation : tag. doc ( ) . map ( |doc| Documentation :: String ( doc. clone ( ) ) ) ,
434- insert_text : Some ( insert_text) ,
493+ text_edit : Some ( tower_lsp_server:: lsp_types:: CompletionTextEdit :: Edit (
494+ TextEdit :: new ( replacement_range, insert_text. clone ( ) ) ,
495+ ) ) ,
435496 insert_text_format : Some ( insert_format) ,
436497 filter_text : Some ( tag. name ( ) . clone ( ) ) ,
437498 sort_text : Some ( format ! ( "1_{}" , tag. name( ) ) ) , // Regular tags sort after end tags
@@ -505,7 +566,7 @@ fn generate_argument_completions(
505566 // Add closing if needed
506567 match closing {
507568 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
508- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
569+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
509570 ClosingBrace :: FullClose => { } // No closing needed
510571 }
511572
@@ -563,7 +624,7 @@ fn generate_argument_completions(
563624 // Add closing if needed
564625 match closing {
565626 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
566- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
627+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
567628 ClosingBrace :: FullClose => { } // No closing needed
568629 }
569630
@@ -614,7 +675,7 @@ fn generate_library_completions(
614675 // Add closing if needed
615676 match closing {
616677 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
617- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
678+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
618679 ClosingBrace :: FullClose => { } // No closing needed
619680 }
620681
@@ -786,7 +847,8 @@ mod tests {
786847 closing : ClosingBrace :: None ,
787848 } ;
788849
789- let completions = generate_template_completions ( & context, None , None , false ) ;
850+ let completions =
851+ generate_template_completions ( & context, None , None , false , Position :: new ( 0 , 0 ) , "" , 0 ) ;
790852
791853 assert ! ( completions. is_empty( ) ) ;
792854 }
@@ -861,4 +923,40 @@ mod tests {
861923 }
862924 ) ;
863925 }
926+
927+ #[ test]
928+ fn test_analyze_template_context_with_auto_paired_brace ( ) {
929+ // Simulates when editor auto-pairs { with } and user types {% if
930+ let line = "{% if}" ;
931+ let cursor_offset = 5 ; // After "if", before the auto-paired }
932+
933+ let context = analyze_template_context ( line, cursor_offset) . expect ( "Should get context" ) ;
934+
935+ assert_eq ! (
936+ context,
937+ TemplateCompletionContext :: TagName {
938+ partial: "if" . to_string( ) ,
939+ needs_space: false ,
940+ closing: ClosingBrace :: PartialClose , // Auto-paired } is detected as PartialClose
941+ }
942+ ) ;
943+ }
944+
945+ #[ test]
946+ fn test_analyze_template_context_with_proper_closing ( ) {
947+ // Proper closing should still be detected
948+ let line = "{% if %}" ;
949+ let cursor_offset = 5 ; // After "if"
950+
951+ let context = analyze_template_context ( line, cursor_offset) . expect ( "Should get context" ) ;
952+
953+ assert_eq ! (
954+ context,
955+ TemplateCompletionContext :: TagName {
956+ partial: "if" . to_string( ) ,
957+ needs_space: false ,
958+ closing: ClosingBrace :: FullClose ,
959+ }
960+ ) ;
961+ }
864962}
0 commit comments