1use std::borrow::Cow;
2use std::path::Path;
3use std::str::FromStr;
4
5use lasso::Spur;
6use ropey::Rope;
7use tower_lsp_server::ls_types::*;
8use tracing::{debug, instrument, trace, warn};
9use tree_sitter::{Node, Parser, QueryCapture, QueryMatch};
10use ts_macros::query;
11
12use crate::prelude::*;
13
14use crate::analyze::{Type, type_cache};
15use crate::index::{_G, _I, _R, PathSymbol, index_models};
16use crate::model::{ModelName, ModelType};
17use crate::xml::determine_csv_xmlid_subgroup;
18use crate::{backend::Backend, backend::Text};
19
20use std::collections::HashMap;
21
22mod completions;
23mod diagnostics;
24
25#[cfg(test)]
26mod tests;
27
28#[rustfmt::skip]
29query! {
30 PyCompletions(Request, XmlId, Mapped, MappedTarget, Depends, ReadFn, Model, Prop, ForXmlId, Scope, FieldDescriptor, FieldType, HasGroups);
31
32(call [
33 (attribute [
34 (identifier) @_env
35 (attribute (_) (identifier) @_env)] (identifier) @_ref)
36 (attribute
37 (identifier) @REQUEST (identifier) @_render)
38 (attribute
39 (_) (identifier) @FOR_XML_ID)
40 (attribute
41 (_) (identifier) @HAS_GROUPS) ]
42 (argument_list . (string) @XML_ID)
43 (#eq? @_env "env")
44 (#eq? @_ref "ref")
45 (#eq? @REQUEST "request")
46 (#eq? @_render "render")
47 (#eq? @FOR_XML_ID "_for_xml_id")
48 (#match? @HAS_GROUPS "^(user_has_groups|has_group)$")
49)
50
51(subscript [
52 (identifier) @_env
53 (attribute (_) (identifier) @_env)]
54 (string) @MODEL
55 (#eq? @_env "env"))
56
57((class_definition
58 (block
59 (expression_statement
60 (assignment
61 (identifier) @PROP [
62 (string) @MODEL
63 (list ((string) @MODEL ","?)*)
64 (call
65 (attribute
66 (identifier) @_fields (identifier) @FIELD_TYPE (#eq? @_fields "fields"))
67 (argument_list
68 . [
69 ((comment)+ (string) @MODEL)
70 (string) @MODEL ]?
71 ((keyword_argument (identifier) @FIELD_DESCRIPTOR (_)) ","?)*)) ])))))
73
74(call [
75 (attribute
76 (_) @MAPPED_TARGET (identifier) @_mapper)
77 (attribute
78 (identifier) @_api (identifier) @DEPENDS)]
79 (argument_list (string) @MAPPED)
80 (#match? @_mapper "^(mapp|filter|sort|group)ed$")
81 (#eq? @_api "api")
82 (#match? @DEPENDS "^(depends|constrains|onchange)$"))
83
84((call
85 (attribute
86 (_) @MAPPED_TARGET (identifier) @_search)
87 (argument_list [
88 (list [
89 (tuple . (string) @MAPPED)
90 (parenthesized_expression (string) @MAPPED)])
91 (keyword_argument
92 (identifier) @_domain
93 (list [
94 (tuple . (string) @MAPPED)
95 (parenthesized_expression (string) @MAPPED)]))]))
96 (#eq? @_domain "domain")
97 (#match? @_search "^(search(_(read|count))?|_?read_group|filtered_domain|_where_calc)$"))
98
99((call
100 (attribute
101 (_) @MAPPED_TARGET (identifier) @READ_FN)
102 (argument_list [
103 (list (string) @MAPPED)
104 (keyword_argument
105 (identifier) @_domain
106 (list (string) @MAPPED)) ]))
107 (#match? @_domain "^(groupby|aggregates)$")
108 (#match? @READ_FN "^(_?read(_group)?|flush_model)$"))
109
110((call
111 (attribute
112 (_) @MAPPED_TARGET (identifier) @DEPENDS)
113 (argument_list . [
114 (set (string) @MAPPED)
115 (dictionary [
116 (pair key: (string) @MAPPED)
117 (ERROR (string) @MAPPED)
118 (ERROR) @MAPPED ])
119 (_ [
120 (set (string) @MAPPED)
121 (dictionary [
122 (pair key: (string) @MAPPED)
123 (ERROR (string) @MAPPED) ]) ]) ]))
124 (#match? @DEPENDS "^(create|write|copy)$"))
125
126((class_definition
127 (block [
128 (function_definition) @SCOPE
129 (decorated_definition
130 (decorator
131 (call
132 (attribute (identifier) @_api (identifier) @_depends)
133 (argument_list ((string) @MAPPED ","?)*)))
134 (function_definition) @SCOPE) ]))
135 (#eq? @_api "api")
136 (#eq? @_depends "depends"))
137
138(class_definition
139 (block
140 (decorated_definition
141 (decorator (_) @_)
142 (function_definition) @SCOPE)*)
143 (#not-match? @_ "^api.depends"))
144}
145
146#[rustfmt::skip]
147query! {
148 PyImports(ImportModule, ImportName, ImportAlias);
149
150(import_from_statement
151 module_name: (dotted_name) @IMPORT_MODULE
152 name: (dotted_name) @IMPORT_NAME)
153
154(import_from_statement
155 module_name: (dotted_name) @IMPORT_MODULE
156 name: (aliased_import
157 name: (dotted_name) @IMPORT_NAME
158 alias: (identifier) @IMPORT_ALIAS))
159
160(import_statement
161 name: (dotted_name) @IMPORT_NAME)
162
163(import_statement
164 name: (aliased_import
165 name: (dotted_name) @IMPORT_NAME
166 alias: (identifier) @IMPORT_ALIAS))
167}
168
169#[derive(derive_more::FromStr, Clone, Copy)]
171#[from_str(rename_all = "snake_case")]
172enum FieldDescriptors {
173 ComodelName,
174 Domain,
175 Compute,
176 Inverse,
177 Search,
178 InverseName,
179 Related,
180 Groups,
181}
182
183pub(crate) fn top_level_stmt(module: Node, offset: usize) -> Option<Node> {
185 module
186 .named_children(&mut module.walk())
187 .find(|child| child.byte_range().contains_end(offset))
188}
189
190fn find_class_definition<'a>(
192 node: tree_sitter::Node<'a>,
193 contents: &str,
194 class_name: &str,
195) -> Option<tree_sitter::Node<'a>> {
196 use crate::utils::PreTravel;
197
198 PreTravel::new(node)
199 .find(|node| {
200 node.kind() == "class_definition"
201 && node
202 .child_by_field_name("name")
203 .map(|name_node| class_name == &contents[name_node.byte_range()])
204 .unwrap_or(false)
205 })
206 .and_then(|node| node.child_by_field_name("name"))
207}
208
209#[derive(Debug)]
210struct Mapped<'text> {
211 needle: &'text str,
212 model: &'text str,
213 single_field: bool,
214 range: ByteRange,
215}
216
217#[derive(Debug, Clone)]
218struct ImportInfo {
219 module_path: String,
220 imported_name: String,
221 alias: Option<String>,
222}
223
224type ImportMap = HashMap<String, ImportInfo>;
225
226impl Backend {
228 fn resolve_import_location(&self, imports: &ImportMap, identifier: &str) -> anyhow::Result<Option<Location>> {
231 let Some(import_info) = imports.get(identifier) else {
232 return Ok(None);
233 };
234
235 if let Some(alias) = &import_info.alias {
237 debug!(
238 "Found aliased import '{}' -> '{}' from module '{}'",
239 alias, import_info.imported_name, import_info.module_path
240 );
241 } else {
242 debug!(
243 "Found direct import '{}' from module '{}'",
244 import_info.imported_name, import_info.module_path
245 );
246 }
247
248 let Some(file_path) = self.index.resolve_py_module(&import_info.module_path) else {
249 debug!("Failed to resolve module path: {}", import_info.module_path);
250 return Ok(None);
251 };
252
253 debug!("Resolved file path: {}", file_path.display());
254
255 let target_contents = ok!(
256 test_utils::fs::read_to_string(&file_path),
257 "Failed to read target file {}",
258 file_path.display(),
259 );
260
261 let class_name = &import_info.imported_name;
262 if let Some(alias) = &import_info.alias {
263 debug!(
264 "Looking for original class '{}' (aliased as '{}') in target file",
265 class_name, alias
266 );
267 } else {
268 debug!("Looking for class '{}' in target file", class_name);
269 }
270
271 let mut target_parser = Parser::new();
272 target_parser
273 .set_language(&tree_sitter_python::LANGUAGE.into())
274 .map_err(|e| anyhow::anyhow!("Failed to set parser language: {}", e))?;
275
276 let Some(target_ast) = target_parser.parse(&target_contents, None) else {
277 debug!("Failed to parse target file with tree-sitter");
278 return Ok(Some(Location {
279 uri: Uri::from_file_path(file_path).unwrap(),
280 range: Range::new(Position::new(0, 0), Position::new(0, 0)),
281 }));
282 };
283
284 if let Some(class_node) = find_class_definition(target_ast.root_node(), &target_contents, class_name) {
285 let range = class_node.range();
286 if let Some(alias) = &import_info.alias {
287 debug!(
288 "Found class '{}' (aliased as '{}') at line {}, col {}",
289 class_name, alias, range.start_point.row, range.start_point.column
290 );
291 } else {
292 debug!(
293 "Found class '{}' at line {}, col {}",
294 class_name, range.start_point.row, range.start_point.column
295 );
296 }
297 return Ok(Some(Location {
298 uri: Uri::from_file_path(file_path).unwrap(),
299 range: span_conv(range),
300 }));
301 }
302
303 if let Some(alias) = &import_info.alias {
304 debug!(
305 "Class '{}' (aliased as '{}') not found in target file using tree-sitter",
306 class_name, alias
307 );
308 } else {
309 debug!("Class '{}' not found in target file using tree-sitter", class_name);
310 }
311 Ok(Some(Location {
312 uri: Uri::from_file_path(file_path).unwrap(),
313 range: Range::new(Position::new(0, 0), Position::new(0, 0)),
314 }))
315 }
316
317 #[tracing::instrument(skip_all, ret, fields(uri))]
318 pub fn on_change_python(
319 &self,
320 text: &Text,
321 uri: &Uri,
322 rope: RopeSlice<'_>,
323 old_rope: Option<Rope>,
324 ) -> anyhow::Result<()> {
325 let mut parser = Parser::new();
326 parser
327 .set_language(&tree_sitter_python::LANGUAGE.into())
328 .expect("bug: failed to init python parser");
329 self.update_ast(text, uri, rope, old_rope, parser)
330 }
331
332 fn parse_imports(&self, contents: &str) -> anyhow::Result<ImportMap> {
334 let mut parser = Parser::new();
335 parser.set_language(&tree_sitter_python::LANGUAGE.into())?;
336
337 let ast = parser
338 .parse(contents, None)
339 .ok_or_else(|| errloc!("Failed to parse Python AST"))?;
340 let query = PyImports::query();
341 let mut cursor = tree_sitter::QueryCursor::new();
342 let mut imports = ImportMap::new();
343
344 debug!("Parsing imports from {} bytes", contents.len());
345
346 let mut matches = cursor.matches(query, ast.root_node(), contents.as_bytes());
347 while let Some(match_) = matches.next() {
348 let mut module_path = None;
349 let mut import_name = None;
350 let mut alias = None;
351
352 debug!("Found import match with {} captures", match_.captures.len());
353
354 for capture in match_.captures {
355 let capture_text = &contents[capture.node.byte_range()];
356 debug!("Capture {}: = '{}'", capture.index, capture_text);
357
358 match PyImports::from(capture.index) {
359 Some(PyImports::ImportModule) => {
360 module_path = Some(capture_text.to_string());
361 }
362 Some(PyImports::ImportName) => {
363 import_name = Some(capture_text.to_string());
364 }
365 Some(PyImports::ImportAlias) => {
366 alias = Some(capture_text.to_string());
367 }
368 _ => {}
369 }
370 }
371
372 if let Some(name) = import_name {
373 let full_module_path = if let Some(module) = module_path {
374 module } else {
376 name.clone() };
378
379 let key = alias.as_ref().unwrap_or(&name).clone();
380 debug!("Adding import: {} -> {} (from module {})", key, name, full_module_path);
381 imports.insert(
382 key,
383 ImportInfo {
384 module_path: full_module_path,
385 imported_name: name,
386 alias,
387 },
388 );
389 }
390 }
391
392 debug!("Final imports map: {:?}", imports);
393 Ok(imports)
394 }
395 pub fn update_models(&self, text: Text, path: &Path, root: Spur, rope: Rope) -> anyhow::Result<()> {
396 let text = match text {
397 Text::Full(text) => Cow::from(text),
398 Text::Delta(_) => Cow::from(rope.slice(..)),
400 };
401 let models = index_models(text.as_bytes())?;
402 let path = PathSymbol::strip_root(root, path);
403 self.index.models.append(path, true, &models);
404 for model in models {
405 match model.type_ {
406 ModelType::Base { name, ancestors } => {
407 let model_key = _G(&name).unwrap();
408 let mut entry = self
409 .index
410 .models
411 .try_get_mut(&model_key)
412 .expect(format_loc!("deadlock"))
413 .unwrap();
414 entry
415 .ancestors
416 .extend(ancestors.into_iter().map(|sym| ModelName::from(_I(&sym))));
417 drop(entry);
418 self.index.models.populate_properties(model_key.into(), &[path]);
419 }
420 ModelType::Inherit(inherits) => {
421 let Some(model) = inherits.first() else { continue };
422 let model_key = _G(model).unwrap();
423 self.index.models.populate_properties(model_key.into(), &[path]);
424 }
425 }
426 }
427 Ok(())
428 }
429 pub async fn did_save_python(&self, uri: Uri, root: Spur) -> anyhow::Result<()> {
430 let path = uri.to_file_path().unwrap();
431 let zone;
432 _ = {
433 let mut document = self
434 .document_map
435 .get_mut(uri.path().as_str())
436 .ok_or_else(|| errloc!("(did_save) did not build document"))?;
437 zone = document.damage_zone.take();
438 let rope = document.rope.clone();
439 let text = Cow::from(&document.rope).into_owned();
440 self.update_models(Text::Full(text), &path, root, rope)
441 }
442 .inspect_err(|err| warn!("{err:?}"));
443 if zone.is_some() {
444 debug!("diagnostics");
445 {
446 let mut document = self.document_map.get_mut(uri.path().as_str()).unwrap();
447 let rope = document.rope.clone();
448 let file_path = uri.to_file_path().unwrap();
449 self.diagnose_python(
450 file_path.to_str().unwrap(),
451 rope.slice(..),
452 zone,
453 &mut document.diagnostics_cache,
454 );
455 let diags = document.diagnostics_cache.clone();
456 self.client.publish_diagnostics(uri, diags, None)
457 }
458 .await;
459 }
460
461 Ok(())
462 }
463 #[instrument(level = "trace", skip_all, ret, fields(range_content = &contents[range.clone()]))]
487 fn gather_mapped<'text>(
488 &self,
489 root: Node,
490 match_: &tree_sitter::QueryMatch,
491 offset: Option<usize>,
492 mut range: core::ops::Range<usize>,
493 this_model: Option<&'text str>,
494 contents: &'text str,
495 for_replacing: bool,
496 single_field_override: Option<bool>,
497 ) -> Option<Mapped<'text>> {
498 let mut needle = if for_replacing {
499 range = range.shrink(1);
500 let offset = offset.unwrap_or(range.end);
501 &contents[range.start..offset]
502 } else {
503 let slice = &contents[range.clone().shrink(1)];
504 let relative_start = range.start + 1;
505 let offset = offset
506 .unwrap_or((range.end - 1).max(relative_start + 1))
507 .max(relative_start)
508 .min(relative_start + slice.len());
509 let start = offset - relative_start;
516 let slice_till_end = slice.get(start..).unwrap_or("");
517 let limit = slice_till_end.find('.').unwrap_or(slice_till_end.len());
519 range = relative_start..offset + limit;
520 &contents[range.clone()]
522 };
523 if needle == "|" || needle == "&" {
524 return None;
525 }
526
527 tracing::trace!("(gather_mapped) {} matches={match_:?}", &contents[range.clone()]);
528
529 let model;
530 if let Some(local_model) = match_.nodes_for_capture_index(PyCompletions::MappedTarget as _).next() {
531 let model_ = (self.index).model_of_range(root, local_model.byte_range().map_unit(ByteOffset), contents)?;
532 model = _R(model_);
533 } else if let Some(this_model) = &this_model {
534 model = this_model
535 } else {
536 return None;
537 }
538
539 let mut single_field = false;
540 if let Some(depends) = match_.nodes_for_capture_index(PyCompletions::Depends as _).next() {
541 single_field = matches!(
542 &contents[depends.byte_range()],
543 "write" | "create" | "constrains" | "onchange"
544 );
545 } else if let Some(read_fn) = match_.nodes_for_capture_index(PyCompletions::ReadFn as _).next() {
546 single_field = true;
548 if contents[read_fn.byte_range()].ends_with("read_group") {
549 needle = match needle.split_once(":") {
551 None => needle,
552 Some((field, _)) => {
553 range = range.start..range.start + field.len();
554 field
555 }
556 }
557 }
558 } else if let Some(override_) = single_field_override {
559 single_field = override_;
560 }
561
562 Some(Mapped {
563 needle,
564 model,
565 single_field,
566 range: range.map_unit(ByteOffset),
567 })
568 }
569 pub fn python_jump_def(
570 &self,
571 params: GotoDefinitionParams,
572 rope: RopeSlice<'_>,
573 ) -> anyhow::Result<Option<Location>> {
574 let uri = ¶ms.text_document_position_params.text_document.uri;
575 let file_path = uri.to_file_path().unwrap();
576 let file_path_str = file_path.to_str().unwrap();
577 let ast = self
578 .ast_map
579 .get(file_path_str)
580 .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?;
581 let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope);
582 let contents = Cow::from(rope);
583 let root = some!(top_level_stmt(ast.root_node(), offset));
584
585 let imports = self.parse_imports(&contents).unwrap_or_default();
587 debug!("Parsed imports: {:?}", imports);
588
589 if let Some(cursor_node) = ast.root_node().descendant_for_byte_range(offset, offset)
591 && cursor_node.kind() == "identifier"
592 {
593 let identifier = &contents[cursor_node.byte_range()];
594 debug!("Checking identifier '{}' at offset {}", identifier, offset);
595
596 if let Some(location) = self.resolve_import_location(&imports, identifier )? {
598 return Ok(Some(location));
599 }
600 }
601
602 let query = PyCompletions::query();
603 let mut cursor = tree_sitter::QueryCursor::new();
604 let mut this_model = ThisModel::default();
605
606 let mut matches = cursor.matches(query, ast.root_node(), contents.as_bytes());
607 while let Some(match_) = matches.next() {
608 for capture in match_.captures {
609 let range = capture.node.byte_range();
610 match PyCompletions::from(capture.index) {
611 Some(PyCompletions::XmlId) if range.contains(&offset) => {
612 let range = range.shrink(1);
613 let slice = Cow::from(ok!(rope.try_slice(range.clone())));
614 let mut slice = slice.as_ref();
615 if match_
616 .nodes_for_capture_index(PyCompletions::HasGroups as _)
617 .next()
618 .is_some()
619 {
620 let mut ref_ = None;
621 determine_csv_xmlid_subgroup(&mut ref_, (slice, range.clone()), offset);
622 (slice, _) = some!(ref_);
623 }
624 return self
625 .index
626 .jump_def_xml_id(slice, ¶ms.text_document_position_params.text_document.uri);
627 }
628 Some(PyCompletions::Model) => {
629 let range = capture.node.byte_range();
630 let is_meta = match_
631 .nodes_for_capture_index(PyCompletions::Prop as _)
632 .next()
633 .map(|prop| matches!(&contents[prop.byte_range()], "_name" | "_inherit"))
634 .unwrap_or(true);
635 if range.contains(&offset) {
636 let range = range.shrink(1);
637 let slice = ok!(rope.try_slice(range.clone()));
638 let slice = Cow::from(slice);
639 return self.index.jump_def_model(&slice);
640 } else if range.end < offset && is_meta
641 {
646 this_model.tag_model(capture.node, match_, root.byte_range(), &contents);
647 }
648 }
649 Some(PyCompletions::Mapped) => {
650 if range.contains_end(offset)
651 && let Some(mapped) = self.gather_mapped(
652 root,
653 match_,
654 Some(offset),
655 range.clone(),
656 this_model.inner,
657 &contents,
658 false,
659 None,
660 ) {
661 let mut needle = mapped.needle;
662 let mut model = _I(mapped.model);
663 if !mapped.single_field {
664 some!(self.index.models.resolve_mapped(&mut model, &mut needle, None).ok());
665 }
666 let model = _R(model);
667 return self.index.jump_def_property_name(needle, model);
668 } else if let Some(cmdlist) = python_next_named_sibling(capture.node)
669 && Backend::is_commandlist(cmdlist, offset)
670 {
671 let (needle, _, model) = some!(self.gather_commandlist(
672 cmdlist,
673 root,
674 match_,
675 offset,
676 range,
677 this_model.inner,
678 &contents,
679 false,
680 ));
681 return self.index.jump_def_property_name(needle, _R(model));
682 }
683 }
684 Some(PyCompletions::FieldDescriptor) => {
685 use FieldDescriptors as FD;
686 let Some(desc_value) = python_next_named_sibling(capture.node) else {
687 continue;
688 };
689 if !desc_value.byte_range().contains_end(offset) {
690 continue;
691 }
692
693 match FD::from_str(&contents[range]) {
694 Ok(FD::ComodelName) => {
695 let range = desc_value.byte_range().shrink(1);
696 let slice = ok!(rope.try_slice(range.clone()));
697 let slice = Cow::from(slice);
698 return self.index.jump_def_model(&slice);
699 }
700 Ok(
701 descriptor @ (FD::Compute | FD::Search | FD::Inverse | FD::Related | FD::InverseName),
702 ) => {
703 let single_field = !matches!(descriptor, FD::Related);
704 let mapped_model = if matches!(descriptor, FD::InverseName) {
705 extract_comodel_name(match_.captures, &contents)
706 .map(|comodel_name| &contents[comodel_name.byte_range().shrink(1)])
707 } else {
708 this_model.inner
709 };
710 let Some(mapped) = self.gather_mapped(
712 root,
713 match_,
714 Some(offset),
715 desc_value.byte_range(),
716 mapped_model,
717 &contents,
718 false,
719 Some(single_field),
720 ) else {
721 break;
722 };
723 let mut needle = mapped.needle;
724 let mut model = _I(mapped.model);
725 if !mapped.single_field {
726 some!(self.index.models.resolve_mapped(&mut model, &mut needle, None).ok());
727 }
728 let model = _R(model);
729 return self.index.jump_def_property_name(needle, model);
730 }
731 Ok(FD::Groups) => {
732 let range = desc_value.byte_range().shrink(1);
733 let value = Cow::from(ok!(rope.try_slice(range.clone())));
734 let mut ref_ = None;
735 determine_csv_xmlid_subgroup(&mut ref_, (&value, range), offset);
736 let (needle, _) = some!(ref_);
737 return self.index.jump_def_xml_id(needle, uri);
738 }
739 Ok(FD::Domain) | Err(_) => {}
740 }
741
742 return Ok(None);
743 }
744 Some(PyCompletions::Request)
745 | Some(PyCompletions::ForXmlId)
746 | Some(PyCompletions::HasGroups)
747 | Some(PyCompletions::XmlId)
748 | Some(PyCompletions::MappedTarget)
749 | Some(PyCompletions::Depends)
750 | Some(PyCompletions::Prop)
751 | Some(PyCompletions::ReadFn)
752 | Some(PyCompletions::Scope)
753 | Some(PyCompletions::FieldType)
754 | None => {}
755 }
756 }
757 }
758
759 let (model, prop, _) = some!(self.attribute_at_offset(offset, root, &contents));
760 self.index.jump_def_property_name(prop, model)
761 }
762 fn attribute_at_offset<'out>(
767 &'out self,
768 offset: usize,
769 root: Node<'out>,
770 contents: &'out str,
771 ) -> Option<(&'out str, &'out str, core::ops::Range<usize>)> {
772 let (lhs, field, range) = Self::attribute_node_at_offset(offset, root, contents)?;
773 let model = (self.index).model_of_range(root, lhs.byte_range().map_unit(ByteOffset), contents)?;
774 Some((_R(model), field, range))
775 }
776 #[instrument(level = "trace", skip_all, ret)]
779 pub fn attribute_node_at_offset<'out>(
780 mut offset: usize,
781 root: Node<'out>,
782 contents: &'out str,
783 ) -> Option<(Node<'out>, &'out str, core::ops::Range<usize>)> {
784 if contents.is_empty() {
785 return None;
786 }
787 offset = offset.clamp(0, contents.len() - 1);
788 let mut cursor_node = root.descendant_for_byte_range(offset, offset)?;
789 let mut real_offset = None;
790 if cursor_node.is_named() && !matches!(cursor_node.kind(), "attribute" | "identifier") {
791 real_offset = Some(offset);
793 offset = offset.saturating_sub(1);
794 cursor_node = root.descendant_for_byte_range(offset, offset)?;
795 }
796 trace!(
797 "(attribute_node_to_offset) {} cursor={}\n sexp={}",
798 &contents[cursor_node.byte_range()],
799 contents.as_bytes()[offset] as char,
800 cursor_node.to_sexp(),
801 );
802 let lhs;
803 let rhs;
804 if !cursor_node.is_named() {
805 let idx = contents[..=offset].bytes().rposition(|c| c == b'.')?;
809 let ident = contents[..=idx].bytes().rposition(|c| c.is_ascii_alphanumeric())?;
810 lhs = root.descendant_for_byte_range(ident, ident)?;
811 rhs = python_next_named_sibling(lhs).and_then(|attr| match attr.kind() {
812 "identifier" => Some(attr),
813 "attribute" => attr.child_by_field_name("attribute"),
814 _ => None,
815 });
816 } else if cursor_node.kind() == "attribute" {
817 lhs = cursor_node.child_by_field_name("object")?;
818 rhs = cursor_node.child_by_field_name("attribute");
819 } else {
820 match cursor_node.parent() {
821 Some(parent) if parent.kind() == "attribute" => {
822 lhs = parent.child_by_field_name("object")?;
823 rhs = Some(cursor_node);
824 }
825 Some(parent) if parent.kind() == "ERROR" => {
826 lhs = cursor_node;
828 rhs = None;
829 }
830 _ => return None,
831 }
832 }
833 trace!(
834 "(attribute_node_to_offset) lhs={} rhs={:?}",
835 &contents[lhs.byte_range()],
836 rhs.as_ref().map(|rhs| &contents[rhs.byte_range()]),
837 );
838 if lhs == cursor_node {
839 return None;
841 }
842 let Some(rhs) = rhs else {
843 let offset = real_offset.unwrap_or(offset);
846 return Some((lhs, "", offset..offset));
847 };
848 let (field, range) = if rhs.range().start_point.row != lhs.range().end_point.row {
849 let offset = real_offset.unwrap_or(offset);
853 ("", offset..offset)
854 } else {
855 let range = rhs.byte_range();
856 (&contents[range.clone()], range)
857 };
858
859 Some((lhs, field, range))
860 }
861 pub fn python_references(
862 &self,
863 params: ReferenceParams,
864 rope: RopeSlice<'_>,
865 ) -> anyhow::Result<Option<Vec<Location>>> {
866 let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope);
867 let uri = ¶ms.text_document_position.text_document.uri;
868 let file_path = uri.to_file_path().unwrap();
869 let file_path_str = file_path.to_str().unwrap();
870 let ast = self
871 .ast_map
872 .get(file_path_str)
873 .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?;
874 let root = some!(top_level_stmt(ast.root_node(), offset));
875 let query = PyCompletions::query();
876 let contents = Cow::from(rope);
877 let mut cursor = tree_sitter::QueryCursor::new();
878 let path = some!(params.text_document_position.text_document.uri.to_file_path());
879 let current_module = self.index.find_module_of(&path);
880 let mut this_model = ThisModel::default();
881
882 let mut matches = cursor.matches(query, ast.root_node(), contents.as_bytes());
883 while let Some(match_) = matches.next() {
884 for capture in match_.captures {
885 let range = capture.node.byte_range();
886 match PyCompletions::from(capture.index) {
887 Some(PyCompletions::XmlId) if range.contains(&offset) => {
888 let range = range.shrink(1);
889 let slice = Cow::from(ok!(rope.try_slice(range.clone())));
890 let mut slice = slice.as_ref();
891 if match_
892 .nodes_for_capture_index(PyCompletions::HasGroups as _)
893 .next()
894 .is_some()
895 {
896 let mut ref_ = None;
897 determine_csv_xmlid_subgroup(&mut ref_, (slice, range.clone()), offset);
898 (slice, _) = some!(ref_);
899 }
900 return self.record_references(&path, slice, current_module);
901 }
902 Some(PyCompletions::Model) => {
903 let range = capture.node.byte_range();
904 let is_meta = match_
905 .nodes_for_capture_index(PyCompletions::Prop as _)
906 .next()
907 .map(|prop| matches!(&contents[prop.byte_range()], "_name" | "_inherit"))
908 .unwrap_or(true);
909 if is_meta && range.contains(&offset) {
910 let range = range.shrink(1);
911 let slice = ok!(rope.try_slice(range.clone()));
912 let slice = Cow::from(slice);
913 let slice = some!(_G(slice));
914 return self.model_references(&path, &slice.into());
915 } else if range.end < offset
916 && match_
917 .nodes_for_capture_index(PyCompletions::FieldType as _)
918 .next()
919 .is_none()
920 {
921 this_model.tag_model(capture.node, match_, root.byte_range(), &contents);
922 }
923 }
924 Some(PyCompletions::FieldDescriptor) => {
925 use FieldDescriptors as FD;
926 let Some(desc_value) = python_next_named_sibling(capture.node) else {
927 continue;
928 };
929 if !desc_value.byte_range().contains_end(offset) {
930 continue;
931 };
932
933 match FD::from_str(&contents[range]) {
934 Ok(FD::ComodelName) => {
935 let range = desc_value.byte_range().shrink(1);
936 let slice = ok!(rope.try_slice(range.clone()));
937 let slice = Cow::from(slice);
938 let slice = some!(_G(slice));
939 return self.model_references(&path, &slice.into());
940 }
941 Ok(FD::Compute | FD::Search | FD::Inverse) => {
942 let range = desc_value.byte_range().shrink(1);
943 let model = some!(this_model.inner.as_ref());
944 let prop = &contents[range];
945 return self.index.method_references(prop, model);
946 }
947 Ok(FD::InverseName) => return Ok(None),
948 Ok(FD::Domain | FD::Related | FD::Groups) | Err(_) => {}
949 }
950
951 return Ok(None);
952 }
953 Some(PyCompletions::Request)
954 | Some(PyCompletions::XmlId)
955 | Some(PyCompletions::ForXmlId)
956 | Some(PyCompletions::HasGroups)
957 | Some(PyCompletions::Mapped)
958 | Some(PyCompletions::MappedTarget)
959 | Some(PyCompletions::Depends)
960 | Some(PyCompletions::Prop)
961 | Some(PyCompletions::ReadFn)
962 | Some(PyCompletions::Scope)
963 | Some(PyCompletions::FieldType)
964 | None => {}
965 }
966 }
967 }
968
969 let (model, prop, _) = some!(self.attribute_at_offset(offset, root, &contents));
970 self.index.method_references(prop, model)
971 }
972
973 pub fn python_hover(&self, params: HoverParams, rope: RopeSlice<'_>) -> anyhow::Result<Option<Hover>> {
974 let uri = ¶ms.text_document_position_params.text_document.uri;
975 let file_path = uri.to_file_path().unwrap();
976 let file_path_str = file_path.to_str().unwrap();
977 let ast = self
978 .ast_map
979 .get(file_path_str)
980 .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?;
981 let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope);
982
983 let contents = Cow::from(rope);
984 let root = some!(top_level_stmt(ast.root_node(), offset));
985 let query = PyCompletions::query();
986 let mut cursor = tree_sitter::QueryCursor::new();
987 let mut this_model = ThisModel::default();
988
989 let mut matches = cursor.matches(query, ast.root_node(), contents.as_bytes());
990 while let Some(match_) = matches.next() {
991 for capture in match_.captures {
992 let range = capture.node.byte_range();
993 match PyCompletions::from(capture.index) {
994 Some(PyCompletions::Model) => {
995 if range.contains_end(offset) {
996 let range = range.shrink(1);
997 let lsp_range = span_conv(capture.node.range());
998 let slice = ok!(rope.try_slice(range.clone()));
999 let slice = Cow::from(slice);
1000 return self.index.hover_model(&slice, Some(lsp_range), false, None);
1001 }
1002 if range.end < offset
1003 && match_
1004 .nodes_for_capture_index(PyCompletions::Prop as _)
1005 .next()
1006 .is_some()
1007 {
1008 this_model.tag_model(capture.node, match_, root.byte_range(), &contents);
1009 }
1010 }
1011 Some(PyCompletions::Mapped) => {
1012 if range.contains(&offset) {
1013 let mapped = some!(self.gather_mapped(
1014 root,
1015 match_,
1016 Some(offset),
1017 range.clone(),
1018 this_model.inner,
1019 &contents,
1020 false,
1021 None,
1022 ));
1023 let mut needle = mapped.needle;
1024 let mut model = _I(mapped.model);
1025 let mut range = mapped.range;
1026 if !mapped.single_field {
1027 some!(
1028 self.index
1029 .models
1030 .resolve_mapped(&mut model, &mut needle, Some(&mut range))
1031 .ok()
1032 );
1033 }
1034 let model = _R(model);
1035 return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope)));
1036 } else if let Some(cmdlist) = python_next_named_sibling(capture.node)
1037 && Backend::is_commandlist(cmdlist, offset)
1038 {
1039 let (needle, range, model) = some!(self.gather_commandlist(
1040 cmdlist,
1041 root,
1042 match_,
1043 offset,
1044 range,
1045 this_model.inner,
1046 &contents,
1047 false,
1048 ));
1049 let range = Some(rope_conv(range, rope));
1050 return self.index.hover_property_name(needle, _R(model), range);
1051 }
1052 }
1053 Some(PyCompletions::XmlId) if range.contains_end(offset) => {
1054 let range = range.shrink(1);
1055 let slice = Cow::from(ok!(rope.try_slice(range.clone())));
1056 let mut slice = slice.as_ref();
1057 if match_
1058 .nodes_for_capture_index(PyCompletions::HasGroups as _)
1059 .next()
1060 .is_some()
1061 {
1062 let mut ref_ = None;
1063 determine_csv_xmlid_subgroup(&mut ref_, (slice, range.clone()), offset);
1064 if let Some((needle, _)) = ref_ {
1065 slice = needle;
1066 }
1067 }
1068 return (self.index).hover_record(slice, Some(rope_conv(range.map_unit(ByteOffset), rope)));
1069 }
1070 Some(PyCompletions::Prop) if range.contains(&offset) => {
1071 let model = some!(this_model.inner);
1072 let name = &contents[range];
1073 let range = span_conv(capture.node.range());
1074 return self.index.hover_property_name(name, model, Some(range));
1075 }
1076 Some(PyCompletions::FieldDescriptor) => {
1077 use FieldDescriptors as FD;
1078 let Some(desc_value) = python_next_named_sibling(capture.node) else {
1079 continue;
1080 };
1081 if !desc_value.byte_range().contains_end(offset) {
1082 continue;
1083 }
1084
1085 match FD::from_str(&contents[range]) {
1086 Ok(FD::ComodelName) => {
1087 let range = desc_value.byte_range().shrink(1);
1088 let lsp_range = span_conv(desc_value.range());
1089 let slice = ok!(rope.try_slice(range.clone()));
1090 let slice = Cow::from(slice);
1091 return self.index.hover_model(&slice, Some(lsp_range), false, None);
1092 }
1093 Ok(
1094 descriptor @ (FD::Compute | FD::Search | FD::Inverse | FD::Related | FD::InverseName),
1095 ) => {
1096 let single_field = !matches!(descriptor, FD::Related);
1097 let mapped_model = if matches!(descriptor, FD::InverseName) {
1098 extract_comodel_name(match_.captures, &contents)
1099 .map(|comodel_name| &contents[comodel_name.byte_range().shrink(1)])
1100 } else {
1101 this_model.inner
1102 };
1103 let mapped = some!(self.gather_mapped(
1104 root,
1105 match_,
1106 Some(offset),
1107 desc_value.byte_range(),
1108 mapped_model,
1109 &contents,
1110 false,
1111 Some(single_field)
1112 ));
1113 let mut needle = mapped.needle;
1114 let mut model = _I(mapped.model);
1115 let mut range = mapped.range;
1116 if !mapped.single_field {
1117 some!(
1118 self.index
1119 .models
1120 .resolve_mapped(&mut model, &mut needle, Some(&mut range))
1121 .ok()
1122 );
1123 }
1124 let model = _R(model);
1125 return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope)));
1126 }
1127 Ok(FD::Groups) => {
1128 let range = desc_value.byte_range().shrink(1);
1129 let value = Cow::from(ok!(rope.try_slice(range.clone())));
1130 let mut ref_ = None;
1131 determine_csv_xmlid_subgroup(&mut ref_, (&value, range), offset);
1132 let (needle, byte_range) = some!(ref_);
1133 return self
1134 .index
1135 .hover_record(needle, Some(rope_conv(byte_range.map_unit(ByteOffset), rope)));
1136 }
1137 Ok(FD::Domain) | Err(_) => {}
1138 }
1139
1140 return Ok(None);
1141 }
1142 Some(PyCompletions::Request)
1143 | Some(PyCompletions::XmlId)
1144 | Some(PyCompletions::ForXmlId)
1145 | Some(PyCompletions::HasGroups)
1146 | Some(PyCompletions::MappedTarget)
1147 | Some(PyCompletions::Depends)
1148 | Some(PyCompletions::ReadFn)
1149 | Some(PyCompletions::Scope)
1150 | Some(PyCompletions::Prop)
1151 | Some(PyCompletions::FieldType)
1152 | None => {}
1153 }
1154 }
1155 }
1156 if let Some((model, prop, range)) = self.attribute_at_offset(offset, root, &contents) {
1157 let lsp_range = Some(rope_conv(range.map_unit(ByteOffset), rope));
1158 return self.index.hover_property_name(prop, model, lsp_range);
1159 }
1160
1161 let root = some!(top_level_stmt(ast.root_node(), offset));
1163 let needle = some!(root.named_descendant_for_byte_range(offset, offset));
1164 let lsp_range = span_conv(needle.range());
1165 let (type_, scope) =
1166 some!((self.index).type_of_range(root, needle.byte_range().map_unit(ByteOffset), &contents));
1167 if let Some(model) = self.index.try_resolve_model(type_cache().resolve(type_), &scope) {
1168 let model = _R(model);
1169 let identifier = (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]);
1170 return self.index.hover_model(model, Some(lsp_range), true, identifier);
1171 }
1172
1173 self.index.hover_variable(
1174 (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]),
1175 type_,
1176 Some(lsp_range),
1177 )
1178 }
1179
1180 pub(crate) fn python_signature_help(&self, params: SignatureHelpParams) -> anyhow::Result<Option<SignatureHelp>> {
1181 use std::fmt::Write;
1182
1183 let uri = ¶ms.text_document_position_params.text_document.uri;
1184 let document = some!((self.document_map).get(uri.path().as_str()));
1185 let file_path = uri.to_file_path().unwrap();
1186 let ast = some!((self.ast_map).get(file_path.to_str().unwrap()));
1187 let contents = Cow::from(&document.rope);
1188
1189 let point = tree_sitter::Point::new(
1190 params.text_document_position_params.position.line as _,
1191 params.text_document_position_params.position.character as _,
1192 );
1193 let node = some!(ast.root_node().descendant_for_point_range(point, point));
1194 let mut args = node;
1195 while let Some(parent) = args.parent() {
1196 if args.kind() == "argument_list" {
1197 break;
1198 }
1199 args = parent;
1200 }
1201
1202 if args.kind() != "argument_list" {
1203 return Ok(None);
1204 }
1205
1206 let active_parameter = 'find_param: {
1207 let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, document.rope.slice(..));
1208 if let Some(contents) = contents.get(..=offset)
1209 && let Some(idx) = contents.bytes().rposition(|c| c == b',' || c == b'(')
1210 {
1211 if contents.as_bytes()[idx] == b'(' {
1212 break 'find_param Some(0);
1213 }
1214 let prev_param = args.descendant_for_byte_range(idx, idx).unwrap().prev_named_sibling();
1215 for (idx, arg) in args.named_children(&mut args.walk()).enumerate() {
1216 if Some(arg) == prev_param {
1217 break 'find_param Some((idx + 1) as u32);
1220 }
1221 }
1222 }
1223
1224 None
1225 };
1226
1227 let callee = some!(args.prev_named_sibling());
1228 let Some((tid, _)) =
1229 (self.index).type_of_range(ast.root_node(), callee.byte_range().map_unit(ByteOffset), &contents)
1230 else {
1231 return Ok(None);
1232 };
1233 let Type::Method(model_key, method) = type_cache().resolve(tid) else {
1234 return Ok(None);
1235 };
1236 let method_key = some!(_G(method));
1237 let rtype = (self.index).eval_method_rtype(method_key.into(), **model_key, None);
1238 let model = some!((self.index).models.get(model_key));
1239 let method_obj = some!(some!(model.methods.as_ref()).get(&method_key));
1240
1241 let mut label = format!("{method}(");
1242 let mut parameters = vec![];
1243
1244 for (idx, param) in method_obj.arguments.as_deref().unwrap_or(&[]).iter().enumerate() {
1245 let begin;
1246 if idx == 0 {
1247 begin = label.len();
1248 _ = write!(&mut label, "{param}");
1249 } else {
1250 begin = label.len() + 2;
1251 _ = write!(&mut label, ", {param}");
1252 }
1253 let end = label.len();
1254 parameters.push(ParameterInformation {
1255 label: ParameterLabel::LabelOffsets([begin as _, end as _]),
1256 documentation: None,
1257 });
1258 }
1259
1260 let rtype = rtype.and_then(|rtype| self.index.type_display(rtype));
1261 match rtype {
1262 Some(rtype) => drop(write!(&mut label, ") -> {rtype}")),
1263 None => label.push_str(") -> ..."),
1264 };
1265
1266 let sig = SignatureInformation {
1267 label,
1268 active_parameter,
1269 parameters: Some(parameters),
1270 documentation: method_obj.docstring.as_ref().map(|doc| {
1271 Documentation::MarkupContent(MarkupContent {
1272 kind: MarkupKind::Markdown,
1273 value: doc.to_string(),
1274 })
1275 }),
1276 };
1277
1278 Ok(Some(SignatureHelp {
1279 signatures: vec![sig],
1280 active_signature: Some(0),
1281 active_parameter: None,
1282 }))
1283 }
1284 pub(crate) fn python_code_action(
1285 &self,
1286 params: CodeActionParams,
1287 rope: RopeSlice<'_>,
1288 ) -> anyhow::Result<Option<CodeActionResponse>> {
1289 let uri = ¶ms.text_document.uri;
1290 let file_path = uri.to_file_path().unwrap();
1291 let file_path_str = file_path.to_str().unwrap();
1292 let ast = self
1293 .ast_map
1294 .get(file_path_str)
1295 .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?;
1296 let ByteOffset(offset) = rope_conv(params.range.end, rope);
1297 let contents = Cow::from(rope);
1298
1299 let query = PyCompletions::query();
1300 let mut cursor = tree_sitter::QueryCursor::new();
1301 let mut matches = cursor.matches(query, ast.root_node(), contents.as_bytes());
1304 while let Some(match_) = matches.next() {
1305 for capture in match_.captures {
1306 let range = capture.node.byte_range();
1307 match PyCompletions::from(capture.index) {
1308 Some(PyCompletions::Model) if range.contains_end(offset) => {
1309 let range = range.shrink(1);
1310 let slice = ok!(rope.try_slice(range.clone()));
1311 let slice = Cow::from(slice);
1312 return self.index.code_action_for_model(&slice, &file_path);
1313 }
1314 _ => {}
1315 }
1316 }
1317 }
1318
1319 Ok(None)
1320 }
1321
1322 fn is_commandlist(cmdlist: Node, offset: usize) -> bool {
1323 matches!(cmdlist.kind(), "list" | "list_comprehension")
1324 && cmdlist.byte_range().contains_end(offset)
1325 && cmdlist.parent().is_some_and(|parent| parent.kind() == "pair")
1326 }
1327 fn gather_commandlist<'text>(
1331 &self,
1332 cmdlist: Node,
1333 root: Node,
1334 match_: &tree_sitter::QueryMatch,
1335 offset: usize,
1336 range: std::ops::Range<usize>,
1337 this_model: Option<&'text str>,
1338 contents: &'text str,
1339 for_replacing: bool,
1340 ) -> Option<(&'text str, ByteRange, Spur)> {
1341 let mut access = contents[range.shrink(1)].to_string();
1342 tracing::debug!(
1343 "gather_commandlist: cmdlist range: {:?}, offset: {}",
1344 cmdlist.byte_range(),
1345 offset
1346 );
1347 let mut dest = cmdlist.descendant_for_byte_range(offset, offset);
1348 tracing::debug!("Initial dest: {:?}", dest.map(|n| (n.kind(), n.byte_range())));
1349
1350 if dest.is_none() && offset > cmdlist.start_byte() {
1353 tracing::debug!("No node at offset {}, trying offset - 1", offset);
1354 if let Some(node) = cmdlist.descendant_for_byte_range(offset - 1, offset - 1) {
1356 tracing::debug!(
1357 "Found node at offset - 1: kind={}, range={:?}",
1358 node.kind(),
1359 node.byte_range()
1360 );
1361 if node.kind() == "string"
1362 || (node.kind() == "string_content" && node.parent().map(|p| p.kind()) == Some("string"))
1363 || node.kind() == "string_end"
1364 {
1365 let string_node = if node.kind() == "string" {
1367 node
1368 } else {
1371 node.parent()?
1372 };
1373 if let Some(next_sibling) = string_node.next_sibling() {
1374 tracing::debug!("String has next sibling: {}", next_sibling.kind());
1375 if next_sibling.kind() != ":" {
1376 dest = Some(string_node);
1378 }
1379 } else {
1380 tracing::debug!("String has no next sibling, treating as incomplete");
1381 dest = Some(string_node);
1383 }
1384 }
1385 }
1386 }
1387
1388 let mut dest = dest?;
1389
1390 if dest.kind() == "string_content" {
1392 dest = dest.parent()?;
1393 }
1394
1395 if dest.kind() != "string" {
1396 dest = dest.parent()?;
1397 }
1398 if dest.kind() != "string" {
1399 return None;
1400 }
1401
1402 let mut is_broken_syntax = false;
1405 if let Some(parent) = dest.parent() {
1406 tracing::debug!("String parent kind: {}", parent.kind());
1407 if parent.kind() == "dictionary" {
1408 if let Some(next_sibling) = dest.next_sibling() {
1410 tracing::debug!("String next sibling kind: {}", next_sibling.kind());
1411 if next_sibling.kind() != ":" {
1412 is_broken_syntax = true;
1413 }
1414 } else {
1415 tracing::debug!("String has no next sibling");
1416 is_broken_syntax = true;
1418 }
1419 } else if parent.kind() == "ERROR" {
1420 if let Some(grandparent) = parent.parent() {
1423 tracing::debug!("ERROR parent (grandparent) kind: {}", grandparent.kind());
1424 if grandparent.kind() == "dictionary" {
1425 is_broken_syntax = true;
1427 }
1428 }
1429 }
1430 }
1431
1432 if is_broken_syntax {
1433 tracing::debug!("Detected broken syntax: string in dictionary without colon");
1434 }
1437
1438 let (needle, model_str, range) = if is_broken_syntax {
1439 let range = ByteRange {
1443 start: ByteOffset(offset),
1444 end: ByteOffset(offset),
1445 };
1446 let model = this_model.unwrap_or("");
1450 ("", model, range)
1451 } else {
1452 let Mapped {
1454 needle, model, range, ..
1455 } = self.gather_mapped(
1456 root,
1457 match_,
1458 Some(offset),
1459 dest.byte_range(),
1460 this_model,
1461 contents,
1462 for_replacing,
1463 None,
1464 )?;
1465 (needle, model, range)
1466 };
1467
1468 tracing::debug!(
1469 "needle={}, is_broken_syntax={}, model_str={}",
1470 needle,
1471 is_broken_syntax,
1472 model_str
1473 );
1474
1475 let mut cursor = cmdlist;
1477 let mut count = 0;
1478 while count < 30 {
1479 count += 1;
1480 let Some(candidate) = cursor.child_with_descendant(dest) else {
1481 tracing::debug!("child_containing_descendant returned None at count={}", count);
1482 return None;
1483 };
1484 let obj;
1485 tracing::debug!("candidate kind: {}", candidate.kind());
1486 if candidate.kind() == "tuple" {
1487 obj = candidate.child_with_descendant(dest)?;
1489 } else if candidate.kind() == "call" {
1490 let args = dig!(candidate, argument_list(1))?;
1492 obj = args.child_with_descendant(dest)?;
1493 } else {
1494 return None;
1495 }
1496 tracing::debug!("obj kind: {}", obj.kind());
1497 if obj.kind() == "dictionary" {
1498 let pair = obj.child_with_descendant(dest)?;
1499 tracing::debug!("pair kind: {}", pair.kind());
1500 if pair.kind() != "pair" {
1501 if pair.kind() == "string" && pair.byte_range().contains(&offset) {
1503 tracing::debug!("Breaking due to broken syntax string in dictionary");
1506 break;
1508 } else if pair.kind() == "ERROR" {
1509 if pair.byte_range().contains(&offset) {
1512 tracing::debug!("Breaking due to ERROR node containing offset");
1513 break;
1514 }
1515 }
1516 tracing::debug!("Returning None: pair kind {} is not 'pair'", pair.kind());
1517 return None;
1518 }
1519
1520 let key = dig!(pair, string)?;
1521 if key.byte_range().contains_end(offset) {
1522 break;
1523 }
1524
1525 cursor = pair.child_with_descendant(dest)?;
1526 access.push('.');
1527 access.push_str(&contents[key.byte_range().shrink(1)]);
1528 } else if obj.kind() == "set" {
1529 break;
1530 } else {
1531 return None;
1533 }
1534 }
1535
1536 if count == 30 {
1537 warn!("recursion limit hit");
1538 }
1539
1540 access.push('.'); tracing::debug!("Access path: {}", access);
1542 tracing::debug!("Initial model before resolve: {}", model_str);
1543 let access = &mut access.as_str();
1544 let mut model = _I(model_str);
1545 if self.index.models.resolve_mapped(&mut model, access, None).is_err() {
1546 tracing::debug!("resolve_mapped failed for model={} access={}", _R(model), access);
1547 return None;
1548 }
1549 tracing::debug!("Resolved model: {}", _R(model));
1550
1551 Some((needle, range, model))
1552 }
1553}
1554
1555#[derive(Default, Clone)]
1556struct ThisModel<'a> {
1557 inner: Option<&'a str>,
1558 source: ThisModelKind,
1559 top_level_range: core::ops::Range<usize>,
1560}
1561
1562#[derive(Default, Clone, Copy)]
1563enum ThisModelKind {
1564 Primary,
1565 #[default]
1566 Inherited,
1567}
1568
1569impl<'this> ThisModel<'this> {
1570 fn tag_model(
1572 &mut self,
1573 model: Node,
1574 match_: &QueryMatch,
1575 top_level_range: core::ops::Range<usize>,
1576 contents: &'this str,
1577 ) {
1578 if match_
1579 .nodes_for_capture_index(PyCompletions::FieldType as _)
1580 .next()
1581 .is_some()
1582 {
1583 return;
1585 }
1586
1587 debug_assert_eq!(model.kind(), "string");
1588 let (is_name, mut is_inherit) = match_
1589 .nodes_for_capture_index(PyCompletions::Prop as _)
1590 .next()
1591 .map(|prop| {
1592 let prop = &contents[prop.byte_range()];
1593 (prop == "_name", prop == "_inherit")
1594 })
1595 .unwrap_or((false, false));
1596 let top_level_changed = top_level_range != self.top_level_range;
1597 is_inherit = is_inherit && (top_level_changed || matches!(self.source, ThisModelKind::Inherited));
1599 if is_inherit {
1600 let parent = model.parent().expect(format_loc!("(tag_model) parent"));
1601 is_inherit = parent.kind() == "assignment" || parent.kind() == "list" && parent.named_child_count() == 1;
1603 }
1604 if is_inherit || is_name && top_level_changed {
1605 self.inner = Some(&contents[model.byte_range().shrink(1)]);
1606 self.top_level_range = top_level_range;
1607 if is_name {
1608 self.source = ThisModelKind::Primary;
1609 } else if is_inherit {
1610 self.source = ThisModelKind::Inherited;
1611 }
1612 }
1613 }
1614}
1615
1616fn extract_string_needle_at_offset<'a>(
1617 rope: RopeSlice<'a>,
1618 range: core::ops::Range<usize>,
1619 offset: usize,
1620) -> anyhow::Result<(Cow<'a, str>, core::ops::Range<ByteOffset>)> {
1621 let slice = rope.try_slice(range.clone())?;
1622 let relative_offset = range.start;
1623 let needle = Cow::from(slice.try_slice(1..offset - relative_offset)?);
1624 let byte_range = range.shrink(1).map_unit(ByteOffset);
1625 Ok((needle, byte_range))
1626}
1627
1628fn extract_comodel_name<'tree>(captures: &[QueryCapture<'tree>], contents: &str) -> Option<Node<'tree>> {
1629 for cap in captures {
1630 match PyCompletions::from(cap.index) {
1631 Some(PyCompletions::Model) => {
1632 if let Some(parent) = cap.node.parent()
1633 && parent.kind() == "argument_list"
1634 {
1635 return Some(cap.node);
1636 }
1637 }
1638 Some(PyCompletions::FieldDescriptor) => {
1639 let Ok(FieldDescriptors::ComodelName) = FieldDescriptors::from_str(&contents[cap.node.byte_range()])
1640 else {
1641 continue;
1642 };
1643 return cap.node.next_named_sibling();
1644 }
1645 _ => {}
1646 }
1647 }
1648
1649 None
1650}