Skip to main content

odoo_lsp/
python.rs

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            // handles `related` `compute` `search` and `inverse`
72            ((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/// Field descriptors that we are interested in providing support.
170#[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
183/// (module (_)*)
184pub(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
190/// Recursively searches for a class definition with the given name in the AST.
191fn 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
226/// Python extensions.
227impl Backend {
228	/// Helper function to resolve import-based jump-to-definition requests.
229	/// Returns the location if successful, None if not found, or an error if resolution fails.
230	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		// Enhanced debugging with alias information
236		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	/// Parse import statements from Python content and return a map of imported names to their module paths
333	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 // For "from module import name", the module path is just the module
375				} else {
376					name.clone() // For "import name", the module path is the name itself
377				};
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			// TODO: Limit range of possible updates based on delta
399			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	/// Gathers common information regarding a mapped access aka dot access.
464	/// Only makes sense in [`PyCompletions`] queries.
465	///
466	/// `single_field_override` should be set in special cases where this function may not have the necessary context to
467	/// determine whether the needle should be processed in mapped mode or single field mode.
468	///
469	/// Replacing:
470	///
471	/// ```text
472	///     "foo.bar.baz"
473	///           ^cursor
474	///      -----------range
475	///      ------needle
476	/// ```
477	///
478	/// Not replacing:
479	///
480	/// ```text
481	///     "foo.bar.baz"
482	///           ^cursor
483	///      -------range
484	///      -------needle
485	/// ```
486	#[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			// assert!(
510			// 	offset >= relative_start,
511			// 	"offset={} cannot be less than relative_start={}",
512			// 	offset,
513			// 	relative_start
514			// );
515			let start = offset - relative_start;
516			let slice_till_end = slice.get(start..).unwrap_or("");
517			// How many characters until the next period or end-of-string?
518			let limit = slice_till_end.find('.').unwrap_or(slice_till_end.len());
519			range = relative_start..offset + limit;
520			// Cow::from(rope.try_slice(range.clone())?)
521			&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			// read or read_group, fields only
547			single_field = true;
548			if contents[read_fn.byte_range()].ends_with("read_group") {
549				// split off aggregate functions
550				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 = &params.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		// Parse imports from the current file
586		let imports = self.parse_imports(&contents).unwrap_or_default();
587		debug!("Parsed imports: {:?}", imports);
588
589		// Check if cursor is on an imported identifier
590		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			// Try to resolve import location
597			if let Some(location) = self.resolve_import_location(&imports, identifier /*, contents_bytes*/)? {
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, &params.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						// match_
642						// 	.nodes_for_capture_index(PyCompletions::FieldType as _)
643						// 	.next()
644						// 	.is_none()
645						{
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								// same as PyCompletions::Mapped
711								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	/// Resolves the attribute and the object's model at the cursor offset
763	/// using [`model_of_range`][Index::model_of_range].
764	///
765	/// Returns `(model, property, range)`.
766	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	/// Resolves the attribute at the cursor offset.
777	/// Returns `(object, field, range)`
778	#[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			// We got our cursor left in the middle of nowhere.
792			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			// We landed on one of the punctuations inside the attribute.
806			// Need to determine which one it is.
807			// We cannot depend on prev_named_sibling because the AST may be all messed up
808			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					// (ERROR (_) @cursor_node)
827					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			// We shouldn't recurse into cursor_node itself.
840			return None;
841		}
842		let Some(rhs) = rhs else {
843			// In single-expression mode, rhs could be empty in which case
844			// we return an empty needle/range.
845			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			// tree-sitter has an issue with attributes spanning multiple lines
850			// which is NOT valid Python, but allows it anyways because tree-sitter's
851			// use cases don't require strict syntax trees.
852			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 = &params.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 = &params.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		// No matches, assume arbitrary expression.
1162		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 = &params.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						// the index might be intentionally out of bounds w.r.t the actual number of arguments
1218						// but this is better than leaving it as None because clients infer it as the first argument
1219						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 = &params.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 this_model = ThisModel::default();
1302
1303		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	/// `cmdlist` must have been checked by [is_commandlist][Backend::is_commandlist] first
1328	///
1329	/// Returns `(needle, range, model)` for a field
1330	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 we can't find a node at the exact offset, try to find the last string node
1351		// This handles the case where cursor is after a string without a colon
1352		if dest.is_none() && offset > cmdlist.start_byte() {
1353			tracing::debug!("No node at offset {}, trying offset - 1", offset);
1354			// Try offset - 1 to see if we're just after a string
1355			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					// Check if this string is not part of a key-value pair (no colon after it)
1366					let string_node = if node.kind() == "string" {
1367						node
1368					// } else if node.kind() == "string_end" && node.parent().map(|p| p.kind()) == Some("string") {
1369					// 	node.parent()?
1370					} 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							// This is an incomplete field name, provide completions
1377							dest = Some(string_node);
1378						}
1379					} else {
1380						tracing::debug!("String has no next sibling, treating as incomplete");
1381						// No next sibling means it's the last element, likely incomplete
1382						dest = Some(string_node);
1383					}
1384				}
1385			}
1386		}
1387
1388		let mut dest = dest?;
1389
1390		// If we're inside a string_content node, get the parent string node
1391		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		// First check if this string is in a broken syntax situation
1403		// (i.e., it's a key in a dictionary without a following colon)
1404		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				// Check if this string has a colon after it
1409				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					// No next sibling means it's the last element, likely incomplete
1417					is_broken_syntax = true;
1418				}
1419			} else if parent.kind() == "ERROR" {
1420				// When there's broken syntax, tree-sitter creates ERROR nodes
1421				// Check if the parent of the ERROR is a dictionary
1422				if let Some(grandparent) = parent.parent() {
1423					tracing::debug!("ERROR parent (grandparent) kind: {}", grandparent.kind());
1424					if grandparent.kind() == "dictionary" {
1425						// This is likely a string in a dictionary with broken syntax
1426						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			// For broken syntax, we need to continue processing to determine the model
1435			// but we'll use an empty needle to show all available fields
1436		}
1437
1438		let (needle, model_str, range) = if is_broken_syntax {
1439			// For broken syntax, we don't want to complete the partial field name
1440			// We want to show all available fields
1441			// We still need to get the model context, so we'll use the parent model if available
1442			let range = ByteRange {
1443				start: ByteOffset(offset),
1444				end: ByteOffset(offset),
1445			};
1446			// Use the this_model if available, otherwise we'll need to determine it from context
1447			// For command lists without explicit model, we need to continue processing
1448			// to determine the model from the field context
1449			let model = this_model.unwrap_or("");
1450			("", model, range)
1451		} else {
1452			// Normal case - complete the field name
1453			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		// recursive descent to collect the chain of fields
1476		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				// (0, 0, {})
1488				obj = candidate.child_with_descendant(dest)?;
1489			} else if candidate.kind() == "call" {
1490				// Command.create({}), but we don't really care if the actual function is called.
1491				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					// Check if this is a broken syntax case (string without colon)
1502					if pair.kind() == "string" && pair.byte_range().contains(&offset) {
1503						// This is a string in a dictionary without a colon
1504						// We're completing field names for this dictionary
1505						tracing::debug!("Breaking due to broken syntax string in dictionary");
1506						// Break out of the loop to resolve the model
1507						break;
1508					} else if pair.kind() == "ERROR" {
1509						// When there's broken syntax, tree-sitter might create an ERROR node
1510						// Check if the ERROR contains our string
1511						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				// TODO: (ERROR) case
1532				return None;
1533			}
1534		}
1535
1536		if count == 30 {
1537			warn!("recursion limit hit");
1538		}
1539
1540		access.push('.'); // to force resolution of the last field
1541		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	/// Call this on captures of index [`PyCompletions::Model`].
1571	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			// debug_assert!(false, "tag_model called on a class model; handle this manually");
1584			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		// If still in same class AND _name already declared, skip.
1598		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			// _inherit = '..' OR _inherit = ['..']
1602			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}