44//! and generating appropriate completion items for Django templates.
55
66use djls_project:: TemplateTags ;
7+ use djls_templates:: templatetags:: generate_snippet_for_tag;
8+ use djls_templates:: templatetags:: TagSpecs ;
79use djls_workspace:: FileKind ;
810use djls_workspace:: PositionEncoding ;
911use djls_workspace:: TextDocument ;
@@ -57,6 +59,8 @@ pub fn handle_completion(
5759 encoding : PositionEncoding ,
5860 file_kind : FileKind ,
5961 template_tags : Option < & TemplateTags > ,
62+ tag_specs : Option < & TagSpecs > ,
63+ supports_snippets : bool ,
6064) -> Vec < CompletionItem > {
6165 // Only handle template files
6266 if file_kind != FileKind :: Template {
@@ -74,7 +78,7 @@ pub fn handle_completion(
7478 } ;
7579
7680 // Generate completions based on available template tags
77- generate_template_completions ( & context, template_tags)
81+ generate_template_completions ( & context, template_tags, tag_specs , supports_snippets )
7882}
7983
8084/// Extract line information from document at given position
@@ -126,49 +130,49 @@ fn get_line_info(
126130
127131/// Analyze a line of template text to determine completion context
128132fn analyze_template_context ( line : & str , cursor_offset : usize ) -> Option < TemplateTagContext > {
129- if cursor_offset > line . chars ( ) . count ( ) {
130- return None ;
131- }
133+ // Find the last {% before cursor position
134+ let prefix = & line [ ..cursor_offset . min ( line . len ( ) ) ] ;
135+ let tag_start = prefix . rfind ( "{%" ) ? ;
132136
133- let chars: Vec < char > = line. chars ( ) . collect ( ) ;
134- let prefix = chars[ ..cursor_offset] . iter ( ) . collect :: < String > ( ) ;
135- let rest_of_line = chars[ cursor_offset..] . iter ( ) . collect :: < String > ( ) ;
136- let rest_trimmed = rest_of_line. trim_start ( ) ;
137-
138- prefix. rfind ( "{%" ) . map ( |tag_start| {
139- let closing_brace = if rest_trimmed. starts_with ( "%}" ) {
140- ClosingBrace :: FullClose
141- } else if rest_trimmed. starts_with ( '}' ) {
142- ClosingBrace :: PartialClose
143- } else {
144- ClosingBrace :: None
145- } ;
137+ // Get the content between {% and cursor
138+ let content_start = tag_start + 2 ;
139+ let content = & prefix[ content_start..] ;
146140
147- let partial_tag_start = tag_start + 2 ; // Skip "{%"
148- let content_after_tag = if partial_tag_start < prefix. len ( ) {
149- & prefix[ partial_tag_start..]
150- } else {
151- ""
152- } ;
141+ // Check if we need a leading space (no space after {%)
142+ let needs_leading_space = content. is_empty ( ) || !content. starts_with ( ' ' ) ;
153143
154- // Check if we need a leading space - true if there's no space after {%
155- let needs_leading_space =
156- !content_after_tag. starts_with ( ' ' ) && !content_after_tag. is_empty ( ) ;
144+ // Extract the partial tag name
145+ let partial_tag = content. trim_start ( ) . to_string ( ) ;
157146
158- let partial_tag = content_after_tag. trim ( ) . to_string ( ) ;
147+ // Check what's after the cursor for closing detection
148+ let suffix = & line[ cursor_offset. min ( line. len ( ) ) ..] ;
149+ let closing_brace = detect_closing_brace ( suffix) ;
159150
160- TemplateTagContext {
161- partial_tag,
162- closing_brace,
163- needs_leading_space,
164- }
151+ Some ( TemplateTagContext {
152+ partial_tag,
153+ closing_brace,
154+ needs_leading_space,
165155 } )
166156}
167157
158+ /// Detect what closing brace is present after the cursor
159+ fn detect_closing_brace ( suffix : & str ) -> ClosingBrace {
160+ let trimmed = suffix. trim_start ( ) ;
161+ if trimmed. starts_with ( "%}" ) {
162+ ClosingBrace :: FullClose
163+ } else if trimmed. starts_with ( '}' ) {
164+ ClosingBrace :: PartialClose
165+ } else {
166+ ClosingBrace :: None
167+ }
168+ }
169+
168170/// Generate Django template tag completion items based on context
169171fn generate_template_completions (
170172 context : & TemplateTagContext ,
171173 template_tags : Option < & TemplateTags > ,
174+ tag_specs : Option < & TagSpecs > ,
175+ supports_snippets : bool ,
172176) -> Vec < CompletionItem > {
173177 let Some ( tags) = template_tags else {
174178 return Vec :: new ( ) ;
@@ -177,25 +181,47 @@ fn generate_template_completions(
177181 let mut completions = Vec :: new ( ) ;
178182
179183 for tag in tags. iter ( ) {
180- // Filter tags based on partial match
181184 if tag. name ( ) . starts_with ( & context. partial_tag ) {
182- // Determine insertion text based on context
183- let mut insert_text = String :: new ( ) ;
184-
185- // Add leading space if needed (cursor right after {%)
186- if context. needs_leading_space {
187- insert_text. push ( ' ' ) ;
188- }
189-
190- // Add the tag name
191- insert_text. push_str ( tag. name ( ) ) ;
192-
193- // Add closing based on what's already present
194- match context. closing_brace {
195- ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
196- ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
197- ClosingBrace :: FullClose => { } // No closing needed
198- }
185+ // Try to get snippet from TagSpecs if available and client supports snippets
186+ let ( insert_text, insert_format) = if supports_snippets {
187+ if let Some ( specs) = tag_specs {
188+ if let Some ( spec) = specs. get ( tag. name ( ) ) {
189+ if spec. args . is_empty ( ) {
190+ // No args, use plain text
191+ build_plain_insert ( tag. name ( ) , context)
192+ } else {
193+ // Generate snippet from tag spec
194+ let mut text = String :: new ( ) ;
195+
196+ // Add leading space if needed
197+ if context. needs_leading_space {
198+ text. push ( ' ' ) ;
199+ }
200+
201+ // Add tag name and snippet arguments
202+ text. push_str ( & generate_snippet_for_tag ( tag. name ( ) , spec) ) ;
203+
204+ // Add closing based on what's already present
205+ match context. closing_brace {
206+ ClosingBrace :: None => text. push_str ( " %}" ) ,
207+ ClosingBrace :: PartialClose => text. push ( '%' ) ,
208+ ClosingBrace :: FullClose => { } // No closing needed
209+ }
210+
211+ ( text, InsertTextFormat :: SNIPPET )
212+ }
213+ } else {
214+ // No spec found, use plain text
215+ build_plain_insert ( tag. name ( ) , context)
216+ }
217+ } else {
218+ // No specs available, use plain text
219+ build_plain_insert ( tag. name ( ) , context)
220+ }
221+ } else {
222+ // Client doesn't support snippets
223+ build_plain_insert ( tag. name ( ) , context)
224+ } ;
199225
200226 // Create completion item
201227 let completion_item = CompletionItem {
@@ -204,7 +230,7 @@ fn generate_template_completions(
204230 detail : Some ( format ! ( "from {}" , tag. library( ) ) ) ,
205231 documentation : tag. doc ( ) . map ( |doc| Documentation :: String ( doc. clone ( ) ) ) ,
206232 insert_text : Some ( insert_text) ,
207- insert_text_format : Some ( InsertTextFormat :: PLAIN_TEXT ) ,
233+ insert_text_format : Some ( insert_format ) ,
208234 filter_text : Some ( tag. name ( ) . clone ( ) ) ,
209235 ..Default :: default ( )
210236 } ;
@@ -216,6 +242,28 @@ fn generate_template_completions(
216242 completions
217243}
218244
245+ /// Build plain insert text without snippets
246+ fn build_plain_insert ( tag_name : & str , context : & TemplateTagContext ) -> ( String , InsertTextFormat ) {
247+ let mut insert_text = String :: new ( ) ;
248+
249+ // Add leading space if needed (cursor right after {%)
250+ if context. needs_leading_space {
251+ insert_text. push ( ' ' ) ;
252+ }
253+
254+ // Add the tag name
255+ insert_text. push_str ( tag_name) ;
256+
257+ // Add closing based on what's already present
258+ match context. closing_brace {
259+ ClosingBrace :: None => insert_text. push_str ( " %}" ) ,
260+ ClosingBrace :: PartialClose => insert_text. push ( '%' ) ,
261+ ClosingBrace :: FullClose => { } // No closing needed
262+ }
263+
264+ ( insert_text, InsertTextFormat :: PLAIN_TEXT )
265+ }
266+
219267#[ cfg( test) ]
220268mod tests {
221269 use super :: * ;
@@ -286,7 +334,7 @@ mod tests {
286334 closing_brace : ClosingBrace :: None ,
287335 } ;
288336
289- let completions = generate_template_completions ( & context, None ) ;
337+ let completions = generate_template_completions ( & context, None , None , false ) ;
290338
291339 assert ! ( completions. is_empty( ) ) ;
292340 }
0 commit comments