|
1 | 1 | use anyhow::{anyhow, Result}; |
| 2 | +use djls_project::TemplateTags; |
2 | 3 | use std::collections::HashMap; |
3 | 4 | use tower_lsp::lsp_types::{ |
4 | | - DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Position, |
5 | | - Range, |
| 5 | + CompletionItem, CompletionItemKind, CompletionResponse, DidChangeTextDocumentParams, |
| 6 | + DidCloseTextDocumentParams, DidOpenTextDocumentParams, Documentation, InsertTextFormat, |
| 7 | + MarkupContent, MarkupKind, Position, Range, |
6 | 8 | }; |
7 | 9 |
|
8 | 10 | #[derive(Debug)] |
@@ -102,6 +104,56 @@ impl Store { |
102 | 104 | pub fn is_version_valid(&self, uri: &str, version: i32) -> bool { |
103 | 105 | self.get_version(uri).map_or(false, |v| v == version) |
104 | 106 | } |
| 107 | + |
| 108 | + pub fn get_completions( |
| 109 | + &self, |
| 110 | + uri: &str, |
| 111 | + position: Position, |
| 112 | + tags: &TemplateTags, |
| 113 | + ) -> Option<CompletionResponse> { |
| 114 | + let document = self.get_document(uri)?; |
| 115 | + |
| 116 | + if document.language_id != LanguageId::HtmlDjango { |
| 117 | + return None; |
| 118 | + } |
| 119 | + |
| 120 | + let context = document.get_template_tag_context(position)?; |
| 121 | + |
| 122 | + let mut completions: Vec<CompletionItem> = tags |
| 123 | + .iter() |
| 124 | + .filter(|tag| { |
| 125 | + context.partial_tag.is_empty() || tag.name().starts_with(&context.partial_tag) |
| 126 | + }) |
| 127 | + .map(|tag| { |
| 128 | + let leading_space = if context.needs_leading_space { " " } else { "" }; |
| 129 | + CompletionItem { |
| 130 | + label: tag.name().to_string(), |
| 131 | + kind: Some(CompletionItemKind::KEYWORD), |
| 132 | + detail: Some(format!("Template tag from {}", tag.library())), |
| 133 | + documentation: tag.doc().as_ref().map(|doc| { |
| 134 | + Documentation::MarkupContent(MarkupContent { |
| 135 | + kind: MarkupKind::Markdown, |
| 136 | + value: doc.to_string(), |
| 137 | + }) |
| 138 | + }), |
| 139 | + insert_text: Some(match context.closing_brace { |
| 140 | + ClosingBrace::None => format!("{}{} %}}", leading_space, tag.name()), |
| 141 | + ClosingBrace::PartialClose => format!("{}{} %", leading_space, tag.name()), |
| 142 | + ClosingBrace::FullClose => format!("{}{} ", leading_space, tag.name()), |
| 143 | + }), |
| 144 | + insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), |
| 145 | + ..Default::default() |
| 146 | + } |
| 147 | + }) |
| 148 | + .collect(); |
| 149 | + |
| 150 | + if completions.is_empty() { |
| 151 | + None |
| 152 | + } else { |
| 153 | + completions.sort_by(|a, b| a.label.cmp(&b.label)); |
| 154 | + Some(CompletionResponse::Array(completions)) |
| 155 | + } |
| 156 | + } |
105 | 157 | } |
106 | 158 |
|
107 | 159 | #[derive(Clone, Debug)] |
@@ -181,6 +233,32 @@ impl TextDocument { |
181 | 233 | pub fn line_count(&self) -> usize { |
182 | 234 | self.index.line_starts.len() |
183 | 235 | } |
| 236 | + |
| 237 | + pub fn get_template_tag_context(&self, position: Position) -> Option<TemplateTagContext> { |
| 238 | + let line = self.get_line(position.line.try_into().ok()?)?; |
| 239 | + let prefix = &line[..position.character.try_into().ok()?]; |
| 240 | + let rest_of_line = &line[position.character.try_into().ok()?..]; |
| 241 | + let rest_trimmed = rest_of_line.trim_start(); |
| 242 | + |
| 243 | + prefix.rfind("{%").map(|tag_start| { |
| 244 | + // Check if we're immediately after {% with no space |
| 245 | + let needs_leading_space = prefix.ends_with("{%"); |
| 246 | + |
| 247 | + let closing_brace = if rest_trimmed.starts_with("%}") { |
| 248 | + ClosingBrace::FullClose |
| 249 | + } else if rest_trimmed.starts_with("}") { |
| 250 | + ClosingBrace::PartialClose |
| 251 | + } else { |
| 252 | + ClosingBrace::None |
| 253 | + }; |
| 254 | + |
| 255 | + TemplateTagContext { |
| 256 | + partial_tag: prefix[tag_start + 2..].trim().to_string(), |
| 257 | + closing_brace, |
| 258 | + needs_leading_space, |
| 259 | + } |
| 260 | + }) |
| 261 | + } |
184 | 262 | } |
185 | 263 |
|
186 | 264 | #[derive(Clone, Debug)] |
@@ -248,3 +326,17 @@ impl From<String> for LanguageId { |
248 | 326 | Self::from(language_id.as_str()) |
249 | 327 | } |
250 | 328 | } |
| 329 | + |
| 330 | +#[derive(Debug)] |
| 331 | +pub enum ClosingBrace { |
| 332 | + None, |
| 333 | + PartialClose, // just } |
| 334 | + FullClose, // %} |
| 335 | +} |
| 336 | + |
| 337 | +#[derive(Debug)] |
| 338 | +pub struct TemplateTagContext { |
| 339 | + pub partial_tag: String, |
| 340 | + pub closing_brace: ClosingBrace, |
| 341 | + pub needs_leading_space: bool, |
| 342 | +} |
0 commit comments