Skip to content

Commit 9224d52

Browse files
committed
Add css class name completions
1 parent c554448 commit 9224d52

File tree

5 files changed

+89
-2
lines changed

5 files changed

+89
-2
lines changed

djlsp/index.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class WorkspaceIndex:
5757
libraries: dict[str, Library] = field(default_factory=dict)
5858
templates: dict[str, Template] = field(default_factory=dict)
5959
global_template_context: dict[str, Variable] = field(default_factory=dict)
60+
css_class_names: list = field(default_factory=list)
6061

6162
def update(self, django_data: dict):
6263
self.file_watcher_globs = django_data.get(
@@ -121,3 +122,4 @@ def update(self, django_data: dict):
121122
)
122123
for name, type_ in django_data.get("global_template_context", {}).items()
123124
}
125+
self.css_class_names = django_data.get("css_class_names", [])

djlsp/parser.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Location,
1616
Position,
1717
Range,
18+
TextEdit,
1819
)
1920
from pygls.workspace import TextDocument
2021

@@ -330,7 +331,8 @@ def completions(self, line, character):
330331
),
331332
)
332333
)
333-
return []
334+
# Check CSS
335+
return self.get_css_class_name_completions(line, character)
334336

335337
def get_load_completions(self, match: Match, **kwargs):
336338
prefix = match.group(1).split(" ")[-1]
@@ -558,6 +560,58 @@ def resolve_completion(item: CompletionItem):
558560

559561
return item
560562

563+
def _absolute_index_to_position(self, index: int) -> Position:
564+
"""Translate an absolute character index into an LSP `Position`."""
565+
running_total = 0
566+
for line_no, text in enumerate(self.document.lines):
567+
line_length = len(text)
568+
if index <= running_total + line_length:
569+
return Position(line=line_no, character=index - running_total)
570+
running_total += line_length
571+
# Fallback to end of document if index is out of bounds
572+
last_line = len(self.document.lines) - 1
573+
last_char = len(self.document.lines[last_line]) if last_line >= 0 else 0
574+
return Position(line=last_line, character=last_char)
575+
576+
def get_css_class_name_completions(self, line, character):
577+
# Flatten text to one line and remove Django template
578+
one_line_html = "".join(self.document.lines)
579+
one_line_html = re.sub(
580+
r"\{\%.*?\%\}", lambda m: " " * len(m.group(0)), one_line_html
581+
)
582+
one_line_html = re.sub(
583+
r"\{\{.*?\}\}", lambda m: " " * len(m.group(0)), one_line_html
584+
)
585+
586+
abs_position = sum(len(self.document.lines[i]) for i in range(line)) + character
587+
class_attr_pattern = re.compile(r'class=["\']([^"\']*)["\']', re.DOTALL)
588+
589+
for match in class_attr_pattern.finditer(one_line_html):
590+
start_idx, end_idx = match.span(1)
591+
592+
if start_idx <= abs_position <= end_idx:
593+
class_value = match.group(1)
594+
relative_pos = abs_position - start_idx
595+
596+
prefix_match = re.search(r"\b[\w-]*$", class_value[:relative_pos])
597+
prefix = prefix_match.group(0) if prefix_match else ""
598+
599+
start_index = abs_position - len(prefix)
600+
start_position = self._absolute_index_to_position(start_index)
601+
end_position = self._absolute_index_to_position(abs_position)
602+
replace_range = Range(start=start_position, end=end_position)
603+
604+
return [
605+
CompletionItem(
606+
label=class_name,
607+
text_edit=TextEdit(range=replace_range, new_text=class_name),
608+
)
609+
for class_name in self.workspace_index.css_class_names
610+
if class_name.startswith(prefix)
611+
]
612+
613+
return []
614+
561615
###################################################################################
562616
# Hover
563617
###################################################################################

djlsp/scripts/django-collector.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def __init__(self, project_src_path):
196196
self.libraries = {}
197197
self.templates: dict[str, Template] = {}
198198
self.global_template_context = {}
199+
self.css_class_names = []
199200

200201
def collect(self):
201202
self.file_watcher_globs = self.get_file_watcher_globs()
@@ -204,6 +205,7 @@ def collect(self):
204205
self.urls = self.get_urls()
205206
self.libraries = self.get_libraries()
206207
self.global_template_context = self.get_global_template_context()
208+
self.css_class_names = self.get_css_class_names()
207209

208210
# Third party collectors
209211
self.collect_for_wagtail()
@@ -217,6 +219,7 @@ def to_json(self):
217219
"libraries": self.libraries,
218220
"templates": self.templates,
219221
"global_template_context": self.global_template_context,
222+
"css_class_names": self.css_class_names,
220223
},
221224
indent=4,
222225
)
@@ -573,6 +576,23 @@ def collect_for_wagtail(self):
573576
model.context_object_name
574577
] = (model.__module__ + "." + model.__name__)
575578

579+
# CSS class names
580+
# ---------------------------------------------------------------------------------
581+
def get_css_class_names(self):
582+
class_pattern = re.compile(r"\.(?!\d)([a-zA-Z0-9_-]+)")
583+
class_names = set()
584+
585+
for finder in get_finders():
586+
for path, file_storage in finder.list(None):
587+
if path.endswith(".css") and not path.startswith("admin"):
588+
try:
589+
with file_storage.open(path, "r") as f:
590+
content = f.read()
591+
class_names.update(class_pattern.findall(content))
592+
except Exception as e:
593+
logger.error(f"Could not parse CSS file: {e}")
594+
return list(class_names)
595+
576596

577597
#######################################################################################
578598
# CLI

djlsp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ def initialized(ls: DjangoTemplateLanguageServer, params: InitializeParams):
433433
@server.feature(
434434
TEXT_DOCUMENT_COMPLETION,
435435
CompletionOptions(
436-
trigger_characters=[" ", "|", "'", '"', "."], resolve_provider=True
436+
trigger_characters=[" ", "|", "'", '"', ".", "-"], resolve_provider=True
437437
),
438438
)
439439
def completions(ls: DjangoTemplateLanguageServer, params: CompletionParams):
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.blog .item {
2+
color: red;
3+
}
4+
5+
.border-red-solid {
6+
border: 1px solid red;
7+
}
8+
9+
.border_blue-solid {
10+
border: 1px solid blue;
11+
}

0 commit comments

Comments
 (0)