@@ -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,6 +338,32 @@ 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. character . saturating_sub ( partial_len as u32 ) ;
351+ let start = Position :: new ( position. line , start_col) ;
352+
353+ // End position: include auto-paired } if present
354+ let mut end_col = position. character ;
355+ if matches ! ( closing, ClosingBrace :: PartialClose ) {
356+ // Include the auto-paired } in the replacement range
357+ // Check if there's a } immediately after cursor
358+ if line_text. len ( ) > cursor_offset && & line_text[ cursor_offset..cursor_offset + 1 ] == "}" {
359+ end_col += 1 ;
360+ }
361+ }
362+ let end = Position :: new ( position. line , end_col) ;
363+
364+ Range :: new ( start, end)
365+ }
366+
325367/// Generate completions for tag names
326368fn generate_tag_name_completions (
327369 partial : & str ,
@@ -330,12 +372,24 @@ fn generate_tag_name_completions(
330372 template_tags : Option < & TemplateTags > ,
331373 tag_specs : Option < & TagSpecs > ,
332374 supports_snippets : bool ,
375+ position : Position ,
376+ line_text : & str ,
377+ cursor_offset : usize ,
333378) -> Vec < CompletionItem > {
334379 let Some ( tags) = template_tags else {
335380 return Vec :: new ( ) ;
336381 } ;
337382
338383 let mut completions = Vec :: new ( ) ;
384+
385+ // Calculate the replacement range for all completions
386+ let replacement_range = calculate_replacement_range (
387+ position,
388+ line_text,
389+ cursor_offset,
390+ partial. len ( ) ,
391+ closing,
392+ ) ;
339393
340394 // First, check if we should suggest end tags
341395 // If partial starts with "end", prioritize end tags
@@ -364,7 +418,9 @@ fn generate_tag_name_completions(
364418 label : end_tag. name . clone ( ) ,
365419 kind : Some ( CompletionItemKind :: KEYWORD ) ,
366420 detail : Some ( format ! ( "End tag for {opener_name}" ) ) ,
367- insert_text : Some ( insert_text) ,
421+ text_edit : Some ( tower_lsp_server:: lsp_types:: CompletionTextEdit :: Edit (
422+ TextEdit :: new ( replacement_range, insert_text. clone ( ) )
423+ ) ) ,
368424 insert_text_format : Some ( InsertTextFormat :: PLAIN_TEXT ) ,
369425 filter_text : Some ( end_tag. name . clone ( ) ) ,
370426 sort_text : Some ( format ! ( "0_{}" , end_tag. name) ) , // Priority sort
@@ -393,14 +449,19 @@ fn generate_tag_name_completions(
393449 text. push ( ' ' ) ;
394450 }
395451
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
452+ // Generate the snippet
453+ let snippet = generate_snippet_for_tag_with_end ( tag. name ( ) , spec) ;
454+ text. push_str ( & snippet) ;
455+
456+ // Only add closing if the snippet doesn't already include it
457+ // (snippets for tags with end tags include their own %} closing)
458+ if !snippet. contains ( "%}" ) {
459+ // Add closing based on what's already present
460+ match closing {
461+ ClosingBrace :: None => text. push_str ( " %}" ) ,
462+ ClosingBrace :: PartialClose => text. push_str ( " %" ) ,
463+ ClosingBrace :: FullClose => { } // No closing needed
464+ }
404465 }
405466
406467 ( text, InsertTextFormat :: SNIPPET )
@@ -419,19 +480,21 @@ fn generate_tag_name_completions(
419480 } ;
420481
421482 // Create completion item
422- // Use SNIPPET kind when we're inserting a snippet, FUNCTION otherwise
483+ // Use SNIPPET kind when we're inserting a snippet, KEYWORD otherwise
423484 let kind = if matches ! ( insert_format, InsertTextFormat :: SNIPPET ) {
424485 CompletionItemKind :: SNIPPET
425486 } else {
426- CompletionItemKind :: FUNCTION
487+ CompletionItemKind :: KEYWORD
427488 } ;
428489
429490 let completion_item = CompletionItem {
430491 label : tag. name ( ) . clone ( ) ,
431492 kind : Some ( kind) ,
432493 detail : Some ( format ! ( "from {}" , tag. library( ) ) ) ,
433494 documentation : tag. doc ( ) . map ( |doc| Documentation :: String ( doc. clone ( ) ) ) ,
434- insert_text : Some ( insert_text) ,
495+ text_edit : Some ( tower_lsp_server:: lsp_types:: CompletionTextEdit :: Edit (
496+ TextEdit :: new ( replacement_range, insert_text. clone ( ) )
497+ ) ) ,
435498 insert_text_format : Some ( insert_format) ,
436499 filter_text : Some ( tag. name ( ) . clone ( ) ) ,
437500 sort_text : Some ( format ! ( "1_{}" , tag. name( ) ) ) , // Regular tags sort after end tags
@@ -479,12 +542,12 @@ fn generate_argument_completions(
479542 if arg. name . starts_with ( partial) {
480543 let mut insert_text = arg. name . clone ( ) ;
481544
482- // Add closing if needed
483- match closing {
484- ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
485- ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
486- ClosingBrace :: FullClose => { } // No closing needed
487- }
545+ // Add closing if needed
546+ match closing {
547+ ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
548+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
549+ ClosingBrace :: FullClose => { } // No closing needed
550+ }
488551
489552 completions. push ( CompletionItem {
490553 label : arg. name . clone ( ) ,
@@ -505,7 +568,7 @@ fn generate_argument_completions(
505568 // Add closing if needed
506569 match closing {
507570 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
508- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
571+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
509572 ClosingBrace :: FullClose => { } // No closing needed
510573 }
511574
@@ -563,7 +626,7 @@ fn generate_argument_completions(
563626 // Add closing if needed
564627 match closing {
565628 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
566- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
629+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
567630 ClosingBrace :: FullClose => { } // No closing needed
568631 }
569632
@@ -614,7 +677,7 @@ fn generate_library_completions(
614677 // Add closing if needed
615678 match closing {
616679 ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
617- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
680+ ClosingBrace :: PartialClose => insert_text. push_str ( " %" ) ,
618681 ClosingBrace :: FullClose => { } // No closing needed
619682 }
620683
@@ -786,7 +849,15 @@ mod tests {
786849 closing : ClosingBrace :: None ,
787850 } ;
788851
789- let completions = generate_template_completions ( & context, None , None , false ) ;
852+ let completions = generate_template_completions (
853+ & context,
854+ None ,
855+ None ,
856+ false ,
857+ Position :: new ( 0 , 0 ) ,
858+ "" ,
859+ 0 ,
860+ ) ;
790861
791862 assert ! ( completions. is_empty( ) ) ;
792863 }
@@ -861,4 +932,41 @@ mod tests {
861932 }
862933 ) ;
863934 }
935+
936+ #[ test]
937+ fn test_analyze_template_context_with_auto_paired_brace ( ) {
938+ // Simulates when editor auto-pairs { with } and user types {% if
939+ let line = "{% if}" ;
940+ let cursor_offset = 5 ; // After "if", before the auto-paired }
941+
942+ let context = analyze_template_context ( line, cursor_offset) . expect ( "Should get context" ) ;
943+
944+ assert_eq ! (
945+ context,
946+ TemplateCompletionContext :: TagName {
947+ partial: "if" . to_string( ) ,
948+ needs_space: false ,
949+ closing: ClosingBrace :: PartialClose , // Auto-paired } is detected as PartialClose
950+ }
951+ ) ;
952+ }
953+
954+ #[ test]
955+ fn test_analyze_template_context_with_proper_closing ( ) {
956+ // Proper closing should still be detected
957+ let line = "{% if %}" ;
958+ let cursor_offset = 5 ; // After "if"
959+
960+ let context = analyze_template_context ( line, cursor_offset) . expect ( "Should get context" ) ;
961+
962+ assert_eq ! (
963+ context,
964+ TemplateCompletionContext :: TagName {
965+ partial: "if" . to_string( ) ,
966+ needs_space: false ,
967+ closing: ClosingBrace :: FullClose ,
968+ }
969+ ) ;
970+ }
971+
864972}
0 commit comments