use std::borrow::Cow;
use std::cmp::Ordering;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed;
use fomat_macros::fomat;
use lasso::Spur;
use tower_lsp_server::ls_types::*;
use tracing::{debug, instrument, warn};
use tree_sitter::Parser;
use xmlparser::{ElementEnd, Error, StrSpan, StreamError, Token, Tokenizer};
use crate::prelude::*;
use crate::analyze::{Scope, Type, normalize, type_cache};
use crate::component::{ComponentTemplate, PropType};
use crate::index::Index;
use crate::model::{Field, FieldKind, PropertyKind};
use crate::record::Record;
use crate::template::gather_templates;
use crate::{ImStr, errloc, format_loc, some, utils::*};
use crate::{backend::Backend, backend::Text};
#[cfg(test)]
mod tests;
const ACTION_MODELS: &[&str] = &[
"ir.actions.act_window",
"ir.actions.report",
"ir.actions.server",
"ir.actions.client",
"ir.actions.act_url",
];
const VIEW_INHERIT_SNIPPET: &str = r#"<record id="" model="ir.ui.view">
<field name="name"></field>
<field name="model">$1</field>
<field name="inherit_id" ref="$2"/>
<field name="arch" type="xml">
<${3:form}>
$0
</$3>
</field>
</record>"#;
#[derive(Debug)]
enum RefKind<'a> {
Ref(&'a str),
Model,
Id,
PropertyName(Vec<(usize, &'a str)>),
MethodName(Vec<(usize, &'a str)>),
TName,
TInherit,
TCall,
PropOf(&'a str),
PyExpr(usize),
Component,
Widget,
ActionTag,
}
enum Tag<'a> {
Field,
Button,
Template,
Record,
Menuitem,
TComponent(&'a str),
}
impl Backend {
pub fn update_xml(
&self,
root: Spur,
text: &Text,
uri: &Uri,
rope: RopeSlice<'_>,
did_save: bool,
) -> anyhow::Result<()> {
let text = match text {
Text::Full(full) => Cow::Borrowed(full.as_str()),
Text::Delta(_) => Cow::from(rope),
};
let mut reader = Tokenizer::from(text.as_ref());
let mut record_ranges = vec![];
let path = uri.to_file_path().ok_or_else(|| errloc!("uri.to_file_path failed"))?;
let current_module = self
.index
.find_module_of(&path)
.ok_or_else(|| errloc!("module_of_path for {} failed", uri.path().as_str()))?;
let mut record_prefix = if did_save {
Some(
self.index
.records
.by_prefix
.write()
.expect(format_loc!("cannot acquire write lock now")),
)
} else {
None
};
let path = uri.to_file_path().unwrap();
let path_uri = PathSymbol::strip_root(root, &path);
loop {
match reader.next() {
Some(Ok(Token::ElementStart { local, span, .. })) => {
let offset = ByteOffset(span.start());
if matches!(local.as_str(), "record" | "template" | "menuitem") {
let record = match local.as_str() {
"record" => Record::from_reader(offset, current_module, path_uri, &mut reader, rope),
"template" => Record::template(offset, current_module, path_uri, &mut reader, rope),
"menuitem" => Record::menuitem(offset, current_module, path_uri, &mut reader, rope),
_ => unreachable!(),
};
let (record, metadata) = match record {
Ok(Some(rec)) => rec,
Ok(None) => {
if local.as_str() != "template" {
continue;
}
let mut entries = vec![];
if let Err(err) = gather_templates(path_uri, &mut reader, rope, &mut entries, true) {
warn!("{err}");
continue;
}
record_ranges.extend(
entries
.into_iter()
.map(|entry| rope_conv(entry.template.location.unwrap().range, rope)),
);
continue;
}
Err(err) => {
warn!(
target: "on_change_xml",
"{local} could not be completely parsed: {}\n{err}",
uri.path().as_str()
);
continue;
}
};
let range = rope_conv(record.location.range, rope);
record_ranges.push(range);
self.index.records.insert(
_I(record.qualified_id()).into(),
record,
metadata,
record_prefix.as_deref_mut(),
);
} else if local.as_str() == "templates" {
let mut entries = vec![];
if let Err(err) = gather_templates(path_uri, &mut reader, rope, &mut entries, false) {
warn!("gather_templates failed: {err}");
continue;
}
record_ranges.extend(
entries
.into_iter()
.map(|entry| rope_conv(entry.template.location.unwrap().range, rope)),
);
}
}
None => break,
Some(Err(err)) => {
debug!("error parsing xml:\n{err}");
break;
}
_ => {}
}
}
self.record_ranges
.insert(uri.path().as_str().to_string(), record_ranges.into_boxed_slice());
Ok(())
}
pub(crate) fn record_slice<'rope>(
&self,
rope: RopeSlice<'rope>,
uri: &Uri,
position: Position,
) -> anyhow::Result<(RopeSlice<'rope>, ByteOffset, usize)> {
let ranges = self
.record_ranges
.get(uri.path().as_str())
.ok_or_else(|| errloc!("Did not build record ranges for {}", uri.path().as_str()))?;
let mut offset_at_cursor = rope_conv(position, rope);
let Ok(record) = ranges.value().binary_search_by(|range| {
if offset_at_cursor < range.start {
Ordering::Greater
} else if offset_at_cursor > range.end {
Ordering::Less
} else {
Ordering::Equal
}
}) else {
debug!("(record_slice) fall back to full slice");
return Ok((rope.slice(..), offset_at_cursor, 0));
};
let record_range = &ranges.value()[record];
debug!(
"(record_slice) found {:?}, cursor={:?}",
record_range.erase(),
offset_at_cursor
);
let relative_offset = record_range.start.0;
offset_at_cursor.0 = offset_at_cursor.0.saturating_sub(relative_offset);
let slice = rope.try_slice(record_range.erase())?;
Ok((slice, offset_at_cursor, relative_offset))
}
pub fn did_save_xml(&self, uri: Uri, root: Spur) -> anyhow::Result<()> {
let rope = {
let document = self
.document_map
.get(uri.path().as_str())
.ok_or_else(|| errloc!("(did_save) did not build document"))?;
document.rope.clone()
};
self.update_xml(root, &Text::Delta(vec![]), &uri, rope.slice(..), true)?;
Ok(())
}
pub fn xml_completions(
&self,
params: &CompletionParams,
rope: RopeSlice<'_>,
) -> anyhow::Result<Option<CompletionResponse>> {
let position = params.text_document_position.position;
let uri = ¶ms.text_document_position.text_document.uri;
let (slice, offset_at_cursor, relative_offset) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let path = some!(uri.to_file_path());
let completions_limit = self
.workspaces
.find_workspace_of(&path, |_, ws| ws.completions.limit)
.unwrap_or_else(|| self.project_config.completions_limit.load(Relaxed));
self.index.xml_completions(
&path,
completions_limit,
rope,
offset_at_cursor,
relative_offset,
&mut reader,
)
}
pub fn xml_is_cursor_in_text(&self, uri: &Uri, position: Position, rope: RopeSlice<'_>) -> anyhow::Result<bool> {
let (slice, ByteOffset(offset_at_cursor), _) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let reader = Tokenizer::from(slice_str.as_ref());
for token in reader {
match token? {
Token::Text { text, .. } if text.range().contains(&offset_at_cursor) => return Ok(true),
other if token_span(&other).range().contains_end(offset_at_cursor) => return Ok(false),
_ => {}
}
}
Ok(false)
}
pub fn xml_jump_def(&self, params: GotoDefinitionParams, rope: RopeSlice<'_>) -> anyhow::Result<Option<Location>> {
let position = params.text_document_position_params.position;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (slice, cursor_by_char, _) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let XmlRefs {
ref_at_cursor,
ref_kind,
model_filter,
scope,
arch_model: _,
} = self.index.gather_refs(cursor_by_char, &mut reader, slice)?;
let Some((mut needle, _)) = ref_at_cursor else {
return Ok(None);
};
match ref_kind {
Some(RefKind::Ref(_)) => self.index.jump_def_xml_id(needle, uri),
Some(RefKind::Model) => self.index.jump_def_model(needle),
Some(RefKind::PropertyName(access)) | Some(RefKind::MethodName(access)) => {
let model_filters = some!(model_filter);
let mut model_filter = some!(model_filters.first()).clone();
let mapped = format!(
"{}.{needle}",
access.iter().map(|(_, prop)| *prop).collect::<Vec<_>>().join(".")
);
if !access.is_empty() {
needle = mapped.as_str();
let mut model = _I(&model_filter);
some!(self.index.models.resolve_mapped(&mut model, &mut needle, None).ok());
model_filter = ImStr::from(_R(model));
}
self.index.jump_def_property_name(needle, &model_filter)
}
Some(RefKind::TInherit) | Some(RefKind::TName) | Some(RefKind::TCall) => {
self.index.jump_def_template_name(needle)
}
Some(RefKind::PropOf(component)) => {
if let Some((handler, _)) = needle.split_once('.') {
needle = handler;
}
self.index.jump_def_component_prop(component, needle)
}
Some(RefKind::Id) => self.index.jump_def_xml_id(needle, uri),
Some(RefKind::PyExpr(py_offset)) => {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let ast = some!(parser.parse(needle, None));
let root = ast.root_node();
let (orig_scope, scope) = Index::walk_scope(root, Some(scope), |scope, node| {
self.index.build_scope(scope, node, py_offset, needle)
});
let scope = scope.unwrap_or(orig_scope);
let (object, field, _) = some!(Self::attribute_node_at_offset(py_offset, ast.root_node(), needle));
let model = some!(self.index.type_of(object, &scope, needle));
let model = type_cache().resolve(model);
let model = some!(self.index.try_resolve_model(model, &Scope::default()));
self.index.jump_def_property_name(field, _R(model))
}
Some(RefKind::Component) => {
let component = some!(_G(needle));
let component = some!(self.index.components.get(&component));
let Some(ComponentTemplate::Name(template)) = component.template.as_ref() else {
return Ok(None);
};
self.index.jump_def_template_name(_R(*template))
}
Some(RefKind::Widget) => self.index.jump_def_widget(needle),
Some(RefKind::ActionTag) => self.index.jump_def_action_tag(needle),
None => Ok(None),
}
}
pub fn xml_references(
&self,
params: ReferenceParams,
rope: RopeSlice<'_>,
) -> anyhow::Result<Option<Vec<Location>>> {
let position = params.text_document_position.position;
let uri = ¶ms.text_document_position.text_document.uri;
let path = some!(uri.to_file_path());
let (slice, cursor_by_char, _) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let XmlRefs {
ref_at_cursor: cursor_value,
ref_kind,
..
} = self.index.gather_refs(cursor_by_char, &mut reader, slice)?;
let (cursor_value, _) = some!(cursor_value);
let current_module = self.index.find_module_of(&path);
match ref_kind {
Some(RefKind::Model) => {
let model = some!(_G(cursor_value));
self.model_references(&path, &model.into())
}
Some(RefKind::Ref(_)) | Some(RefKind::Id) => self.record_references(&path, cursor_value, current_module),
Some(RefKind::TInherit) | Some(RefKind::TCall) => self.index.template_references(cursor_value, true),
Some(RefKind::TName) => self.index.template_references(cursor_value, false),
Some(RefKind::PyExpr(_))
| Some(RefKind::PropertyName(_))
| Some(RefKind::MethodName(_))
| Some(RefKind::PropOf(..))
| Some(RefKind::Component)
| Some(RefKind::Widget)
| Some(RefKind::ActionTag)
| None => Ok(None),
}
}
pub fn xml_hover(&self, params: HoverParams, rope: RopeSlice<'_>) -> anyhow::Result<Option<Hover>> {
let position = params.text_document_position_params.position;
let uri = ¶ms.text_document_position_params.text_document.uri;
let path = some!(uri.to_file_path());
let (slice, offset_at_cursor, relative_offset) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let XmlRefs {
ref_at_cursor,
ref_kind,
model_filter,
scope,
arch_model: _,
} = self.index.gather_refs(offset_at_cursor, &mut reader, slice)?;
let (mut needle, ref_range) = some!(ref_at_cursor);
let mut lsp_range = Some(rope_conv(
ref_range.clone().map_unit(|unit| ByteOffset(unit + relative_offset)),
rope,
));
let current_module = self.index.find_module_of(&path);
match ref_kind {
Some(RefKind::Model) => self.index.hover_model(needle, lsp_range, false, None),
Some(RefKind::Ref(_)) => {
if needle.contains('.') {
self.index.hover_record(needle, lsp_range)
} else {
let current_module = some!(current_module);
let xml_id = format!("{}.{}", _R(current_module), needle);
self.index.hover_record(&xml_id, lsp_range)
}
}
Some(RefKind::PropertyName(access)) | Some(RefKind::MethodName(access)) => {
let model_filters = some!(model_filter);
let mut model_filter = some!(model_filters.first()).clone();
let mapped = format!(
"{}.{needle}",
access.iter().map(|(_, field)| *field).collect::<Vec<_>>().join(".")
);
if !access.is_empty() {
needle = mapped.as_str();
let mut model = _I(&model_filter);
some!(self.index.models.resolve_mapped(&mut model, &mut needle, None).ok());
model_filter = ImStr::from(_R(model));
}
self.index.hover_property_name(needle, &model_filter, lsp_range)
}
Some(RefKind::Id) => {
let current_module = some!(current_module);
let xml_id = if needle.contains('.') {
Cow::from(needle)
} else {
format!("{}.{}", _R(current_module), needle).into()
};
self.index.hover_record(&xml_id, lsp_range)
}
Some(RefKind::TInherit) | Some(RefKind::TCall) => Ok(self.index.hover_template(needle, lsp_range)),
Some(RefKind::PyExpr(py_offset)) => {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let ast = some!(parser.parse(needle, None));
let root = ast.root_node();
let (tid, scope) = some!(self.index.type_of_node(
Some(scope),
root,
ByteOffset(py_offset)..ByteOffset(py_offset),
needle
));
let mut display = needle;
if let Some(ident) = root.descendant_for_byte_range(py_offset, py_offset)
&& ident.kind() == "identifier"
{
display = &slice_str[ident.byte_range().map_unit(|it| it + ref_range.start)];
}
if let Some(model) = (self.index).try_resolve_model(type_cache().resolve(tid), &scope) {
return self.index.hover_model(_R(model), lsp_range, true, Some(display));
} else if let Some((object, field, _range)) = Self::attribute_node_at_offset(py_offset, root, needle) {
let model = some!(self.index.type_of(object, &scope, needle));
let model = type_cache().resolve(model);
let model = some!(self.index.try_resolve_model(model, &scope));
return self.index.hover_property_name(field, _R(model), None);
}
let type_ = self.index.type_display(tid);
let type_ = type_.as_deref().unwrap_or("Any");
Ok(Some(Hover {
range: lsp_range,
contents: HoverContents::Scalar(MarkedString::from_language_code(
"python".to_string(),
format!("(local) {display}: {type_}"),
)),
}))
}
Some(RefKind::Component) => Ok(self.index.hover_component(needle, lsp_range)),
Some(RefKind::PropOf(component_key)) => {
if let Some((handler, _)) = needle.split_once('.') {
if let Some(lsp_range) = lsp_range.as_mut() {
lsp_range.end.character = lsp_range.start.character + handler.len() as u32;
}
needle = handler;
}
let prop = some!(_G(needle));
let component = some!(_G(component_key));
let component = some!(self.index.components.get(&component));
let field = some!(component.props.get(&prop.into()));
let type_descriptor = field.type_ & !(PropType::Optional | PropType::Array);
let needs_paren = field.type_.contains(PropType::Array) && type_descriptor.iter().count() > 1;
let contents = fomat! {
"(property) " (component_key) "." (needle)
if field.type_.contains(PropType::Optional) { "?" } ": "
if needs_paren { "(" }
for type_ in type_descriptor.iter() {
match type_ {
PropType::String => { "string" }
PropType::Number => { "number" }
PropType::Boolean => { "boolean" }
PropType::Object => { "object" }
PropType::Function => { "Function" }
_ => { "unknown" }
}
}
separated { " | " }
if needs_paren { ")" }
if field.type_.contains(PropType::Array) { "[]" }
};
Ok(Some(Hover {
range: lsp_range,
contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
language: "ts".to_string(),
value: contents,
})),
}))
}
Some(RefKind::TName) | Some(RefKind::Widget) | Some(RefKind::ActionTag) | None => {
#[cfg(not(debug_assertions))]
return Ok(None);
#[cfg(debug_assertions)]
{
let contents = format!("{:#?}", (needle, ref_kind, model_filter));
Ok(Some(Hover {
contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
language: "rust".to_string(),
value: contents,
})),
range: lsp_range,
}))
}
}
}
}
pub fn xml_code_actions(
&self,
params: CodeActionParams,
rope: RopeSlice<'_>,
) -> anyhow::Result<Option<CodeActionResponse>> {
let uri = ¶ms.text_document.uri;
let position = params.range.start;
let (slice, offset_at_cursor, _) = self.record_slice(rope, uri, position)?;
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let XmlRefs {
ref_at_cursor,
ref_kind,
..
} = self.index.gather_refs(offset_at_cursor, &mut reader, slice)?;
let (Some((value, _)), Some(RefKind::Component)) = (ref_at_cursor, ref_kind) else {
return Ok(None);
};
Ok(Some(vec![CodeActionOrCommand::Command(Command {
title: "Go to Owl component".to_string(),
command: "goto_owl".to_string(),
arguments: Some(vec![String::new().into(), value.into()]),
})]))
}
pub(crate) fn xml_debug_inspect_type(
&self,
params: TextDocumentPositionParams,
rope: RopeSlice<'_>,
) -> anyhow::Result<Option<String>> {
let position = params.position;
let uri = ¶ms.text_document.uri;
let (slice, offset_at_cursor, _) = some!(self.record_slice(rope.slice(..), uri, position).ok());
let slice_str = Cow::from(slice);
let mut reader = Tokenizer::from(slice_str.as_ref());
let XmlRefs {
ref_at_cursor,
ref_kind,
scope,
..
} = self.index.gather_refs(offset_at_cursor, &mut reader, slice)?;
let (Some((needle, _)), Some(RefKind::PyExpr(py_offset))) = (ref_at_cursor, ref_kind) else {
return Ok(None);
};
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let ast = some!(parser.parse(needle, None));
let root = ast.root_node();
let (type_, _) =
some!((self.index).type_of_node(Some(scope), root, ByteOffset(py_offset)..ByteOffset(py_offset), needle));
Ok(Some(format!("{type_:?}").replacen("Text::", "", 1)))
}
}
impl Index {
fn xml_completions<'read>(
&self,
path: &Path,
completions_limit: usize,
rope: RopeSlice<'read>,
offset_at_cursor: ByteOffset,
relative_offset: usize,
reader: &mut Tokenizer<'read>,
) -> Result<Option<CompletionResponse>, anyhow::Error> {
let current_module = self.find_module_of(path).expect("must be in a module");
let mut items = MaxVec::new(completions_limit);
let XmlRefs {
ref_at_cursor,
ref_kind,
model_filter,
scope,
arch_model,
} = self.gather_refs(offset_at_cursor, reader, rope)?;
let (Some((value, value_range)), Some(record_field)) = (ref_at_cursor, ref_kind) else {
return Ok(None);
};
let mut needle = if offset_at_cursor.0 >= value_range.end {
value
} else if offset_at_cursor.0 >= value_range.start {
&value[..offset_at_cursor.0 - value_range.start]
} else {
""
};
let replace_range = value_range.clone().map_unit(|unit| ByteOffset(unit + relative_offset));
match record_field {
RefKind::Ref(relation) => {
let model_filters = some!(model_filter);
let model_key = some!(model_filters.first());
let model = some!(_G(model_key));
let relation = {
let fields = some!(self.models.populate_properties(model.into(), &[])).downgrade();
let fields = some!(fields.fields.as_ref());
let relation = some!(_G(relation));
let Some(Field {
kind: FieldKind::Relational(relation),
..
}) = fields.get(&relation).map(Arc::as_ref)
else {
return Ok(None);
};
*relation
};
self.complete_xml_id(
needle,
replace_range,
rope,
Some(&[ImStr::from(_R(relation))]),
current_module,
arch_model.map(|model| _I(&model).into()),
&mut items,
)?
}
RefKind::Model => {
let range = rope_conv(replace_range, rope);
self.complete_model(needle, range, &mut items)?
}
ref ref_kind @ RefKind::PropertyName(ref access) | ref ref_kind @ RefKind::MethodName(ref access) => {
let model_filters = some!(model_filter);
let mut model_filter = some!(model_filters.first()).clone();
let mapped = format!(
"{}.{needle}",
access.iter().map(|(_, field)| *field).collect::<Vec<_>>().join(".")
);
if !access.is_empty() {
needle = mapped.as_str();
let mut model = _I(&model_filter);
some!(self.models.resolve_mapped(&mut model, &mut needle, None).ok());
model_filter = ImStr::from(_R(model));
}
let for_only_prop = Some(if matches!(ref_kind, RefKind::MethodName(_)) {
PropertyKind::Method
} else {
PropertyKind::Field
});
self.complete_property_name(
needle,
replace_range,
model_filter,
rope,
for_only_prop,
None,
true,
false,
&mut items,
)?;
}
RefKind::TInherit | RefKind::TCall => {
let range = rope_conv(replace_range, rope);
self.complete_template_name(needle, range, &mut items)?;
}
RefKind::PropOf(component) => {
self.complete_component_prop( replace_range, rope, component, &mut items)?;
}
RefKind::Id => {
self.complete_xml_id(
needle,
replace_range,
rope,
model_filter.as_deref(),
current_module,
None,
&mut items,
)?;
}
RefKind::PyExpr(py_offset) => 'expr: {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let ast = some!(parser.parse(value, None));
let Some((object, field, range)) = Backend::attribute_node_at_offset(py_offset, ast.root_node(), value)
else {
items.extend(scope.iter().map(|(ident, model)| {
let Some(model) = self.try_resolve_model(model, &scope) else {
return CompletionItem {
label: ident.to_string(),
kind: Some(CompletionItemKind::VARIABLE),
..Default::default()
};
};
let model_name = _R(model);
let documentation = self.models.get(&model).map(|model| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: self.model_docstring(&model, Some(model_name), Some(ident)),
})
});
CompletionItem {
label: ident.to_string(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some(model_name.to_string()),
documentation,
..Default::default()
}
}));
break 'expr;
};
let (default_scope, scope) = Index::walk_scope(ast.root_node(), Some(scope.clone()), |scope, node| {
self.build_scope(scope, node, range.end, value)
});
let scope = scope.unwrap_or(default_scope);
let model = some!(self.type_of(object, &scope, value));
let model = type_cache().resolve(model);
let model = some!(self.try_resolve_model(model, &scope));
let needle_end = py_offset.saturating_sub(range.start);
let mut needle = field;
if !needle.is_empty() && needle_end < needle.len() {
needle = &needle[..needle_end];
}
let anchor = value_range.start + relative_offset;
self.complete_property_name(
needle,
range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)),
ImStr::from(_R(model)),
rope,
None, None,
false,
false,
&mut items,
)?;
}
RefKind::Widget => {
self.complete_widget( replace_range, rope, &mut items)?;
}
RefKind::ActionTag => {
self.complete_action_tag( replace_range, rope, &mut items)?;
}
RefKind::TName | RefKind::Component => return Ok(None),
}
Ok(Some(CompletionResponse::List(CompletionList {
is_incomplete: !items.has_space(),
items: items.into_inner(),
})))
}
#[instrument(level = "trace", skip_all, ret)]
fn gather_refs<'read>(
&self,
offset_at_cursor: ByteOffset,
reader: &mut Tokenizer<'read>,
slice: RopeSlice<'read>,
) -> anyhow::Result<XmlRefs<'read>> {
let mut tag = None;
let mut ref_at_cursor = None::<(&str, core::ops::Range<usize>)>;
let mut ref_kind = None;
let mut model_filter = None;
let mut template_mode = false;
let offset_at_cursor = offset_at_cursor.0;
let mut arch_model = None;
let mut arch_mode = false;
let mut arch_depth = 0;
let mut depth = 0;
let mut expect_model_string = false;
let mut expect_template_string = false;
let mut expect_action_tag = false;
let mut button_type: Option<&str> = None;
let mut scope = Scope::default();
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let mut foreach_as = attr_pair("t-foreach", "t-as");
let mut set_value = attr_pair("t-set", "t-value");
struct FieldAccess<'a> {
field: StrSpan<'a>,
depth: u32,
}
let mut accesses: Vec<FieldAccess> = vec![];
for token in reader {
match token {
Ok(Token::ElementStart { local, prefix, .. }) => {
expect_model_string = false;
depth += 1;
match local.as_str() {
"field" => tag = Some(Tag::Field),
"button" => {
tag = Some(Tag::Button);
button_type = None;
}
"template" => {
tag = Some(Tag::Template);
model_filter = Some(vec![ImStr::from("ir.ui.view")]);
}
"record" => tag = Some(Tag::Record),
"menuitem" => tag = Some(Tag::Menuitem),
"templates" => {
template_mode = true;
}
component if template_mode && component.starts_with(|c| char::is_ascii_uppercase(&c)) => {
tag = Some(Tag::TComponent(component));
if prefix.is_empty() && local.range().contains_end(offset_at_cursor) {
ref_at_cursor = Some((local.as_str(), local.range()));
ref_kind = Some(RefKind::Component);
}
}
_ if arch_mode => tag = None,
_ => {}
}
}
Ok(Token::Attribute { local, value, .. }) if matches!(tag, Some(Tag::Field)) => {
let value_in_range = value.range().contains_end(offset_at_cursor);
match local.as_str() {
"ref" if value_in_range => {
ref_at_cursor = Some((value.as_str(), value.range()));
}
"name" if value_in_range => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::PropertyName(
accesses
.iter()
.map(|access| (access.field.start(), access.field.as_str()))
.collect(),
));
}
"name" if matches!(value.as_str(), "model" | "res_model") => {
expect_model_string = true;
}
"name" if value.as_str() == "report_name" => {
expect_template_string = true;
}
"name" if value.as_str() == "tag" => {
expect_action_tag = true;
}
"name" if value.as_str() == "arch" => {
arch_mode = true;
arch_depth = depth
}
"name" => {
ref_kind = Some(RefKind::Ref(value.as_str()));
if arch_mode {
accesses.push(FieldAccess { field: value, depth });
}
}
"position" => {
match accesses.last() {
Some(access) if depth == access.depth => {
accesses.pop();
}
_ => {}
}
}
"groups" if value_in_range => {
ref_kind = Some(RefKind::Id);
model_filter = Some(vec![ImStr::from("res.groups")]);
arch_model = None;
determine_csv_xmlid_subgroup_of_xmlspan(&mut ref_at_cursor, value, offset_at_cursor);
}
"widget" if value_in_range && arch_depth > 0 => {
ref_kind = Some(RefKind::Widget);
ref_at_cursor = Some((value.as_str(), value.range()));
}
_ => {}
}
}
Ok(Token::Attribute { local, value, .. }) if matches!(tag, Some(Tag::Button)) => match local.as_str() {
"type" => {
button_type = Some(value.as_str());
}
"name" if value.range().contains_end(offset_at_cursor) => {
if value.as_str().starts_with("%(") {
if value.as_str().ends_with(")d") {
let inner = &value.as_str()[2..value.as_str().len() - 2];
let start_offset = value.range().start + 2;
let end_offset = value.range().end - 2;
ref_at_cursor = Some((inner, start_offset..end_offset));
} else {
let inner = &value.as_str()[2..];
let start_offset = value.range().start + 2;
let end_offset = value.range().end;
ref_at_cursor = Some((inner, start_offset..end_offset));
}
ref_kind = Some(RefKind::Id);
model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect());
} else if button_type == Some("action") {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Id);
model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect());
} else {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::MethodName(vec![]));
}
}
_ => {}
},
Ok(Token::Attribute { local, value, .. })
if matches!(tag, Some(Tag::Template))
&& value.range().contains_end(offset_at_cursor)
&& matches!(local.as_str(), "inherit_id" | "t-call") =>
{
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Ref("inherit_id"));
}
Ok(Token::Attribute { local, value, .. })
if matches!(tag, Some(Tag::Record)) && local.as_str() == "model" =>
{
if value.range().contains_end(offset_at_cursor) {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Model);
} else {
model_filter = Some(vec![ImStr::from(value.as_str())]);
}
}
Ok(Token::Attribute { local, value, .. })
if matches!(tag, Some(Tag::Record | Tag::Template | Tag::Menuitem))
&& local == "id" && value.range().contains_end(offset_at_cursor) =>
{
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Id);
arch_model = None;
match tag {
Some(Tag::Template) => {
model_filter = Some(vec![ImStr::from("ir.ui.view")]);
}
Some(Tag::Menuitem) => {
model_filter = Some(vec![ImStr::from("ir.ui.menu")]);
}
_ => {}
}
}
Ok(Token::Attribute { local, value, .. })
if matches!(tag, Some(Tag::Menuitem)) && value.range().contains_end(offset_at_cursor) =>
{
match local.as_str() {
"parent" => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Ref("parent_id"));
model_filter = Some(vec![ImStr::from("ir.ui.menu")]);
}
"action" => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::Id);
model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect());
}
"groups" => {
ref_kind = Some(RefKind::Id);
arch_model = None;
model_filter = Some(vec!["res.groups".into()]);
determine_csv_xmlid_subgroup_of_xmlspan(&mut ref_at_cursor, value, offset_at_cursor);
}
_ => {}
}
}
Ok(Token::Attribute { local, .. })
if matches!(tag, Some(Tag::TComponent(..))) && local.range().contains_end(offset_at_cursor) =>
{
let Some(Tag::TComponent(component)) = tag else {
unreachable!()
};
ref_at_cursor = Some((local.as_str(), local.range()));
ref_kind = Some(RefKind::PropOf(component));
}
Ok(Token::Attribute { local, value, .. }) if value.range().contains_end(offset_at_cursor) => {
match local.as_str() {
"t-name" => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::TName);
}
"t-inherit" => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::TInherit);
}
"t-call" => {
ref_at_cursor = Some((value.as_str(), value.range()));
ref_kind = Some(RefKind::TCall);
}
t_attr if t_attr.starts_with("t-") => {
ref_at_cursor = Some((value.as_str(), value.range()));
let py_offset = offset_at_cursor.saturating_sub(value.start());
ref_kind = Some(RefKind::PyExpr(py_offset));
}
"groups" => {
ref_kind = Some(RefKind::Id);
model_filter = Some(vec![ImStr::from("res.groups")]);
arch_model = None;
determine_csv_xmlid_subgroup_of_xmlspan(&mut ref_at_cursor, value, offset_at_cursor);
}
_ => {}
}
}
Ok(Token::Attribute { local, value, .. }) => {
(foreach_as.accept(local.as_str(), value)).and_then(|((foreach, _), (as_, _))| {
let ast = parser.parse(&*foreach, None)?;
self.insert_in_scope(&mut scope, &as_, ast.root_node(), &foreach, true)
.inspect_err(|err| {
debug!("(gather_refs) foreach_as failed: {err}");
})
.ok()
});
(set_value.accept(local.as_str(), value)).and_then(|((set, _), (value, _))| {
let ast = parser.parse(&*value, None)?;
_ = self
.insert_in_scope(&mut scope, &set, ast.root_node(), &value, false)
.inspect_err(|err| {
debug!("(gather_refs) set_value failed: {err}");
});
Some(())
});
if local.as_str() == "t-name" && !template_mode {
template_mode = true;
}
}
Ok(Token::Text { text }) if expect_model_string => {
expect_model_string = false;
if text.range().contains_end(offset_at_cursor) {
ref_at_cursor = Some((text.as_str(), text.range()));
ref_kind = Some(RefKind::Model);
} else {
arch_model = Some(text);
}
}
Ok(Token::Text { text }) if expect_template_string => {
expect_template_string = false;
if text.range().contains_end(offset_at_cursor) {
ref_at_cursor = Some((text.as_str(), text.range()));
ref_kind = Some(RefKind::Ref("inherit_id"));
model_filter = Some(vec![ImStr::from("ir.ui.view")]);
}
}
Ok(Token::Text { text }) if expect_action_tag => {
expect_action_tag = false;
if text.range().contains_end(offset_at_cursor) {
ref_at_cursor = Some((text.as_str(), text.range()));
ref_kind = Some(RefKind::ActionTag);
}
}
Ok(Token::ElementEnd { end, span }) => {
foreach_as.reset();
set_value.reset();
if let ElementEnd::Close(..) | ElementEnd::Empty = end {
if depth == arch_depth {
arch_mode = false;
}
match accesses.last() {
Some(access) if access.depth == depth => {
accesses.pop();
}
_ => {}
}
if matches!(tag, Some(Tag::Button)) {
button_type = None;
tag = None;
}
depth -= 1;
}
if template_mode
&& matches!(end, ElementEnd::Open | ElementEnd::Empty)
&& span.start() > offset_at_cursor
&& let Some(c) = slice.get_byte(offset_at_cursor)
&& c.is_ascii_whitespace()
&& let Some(Tag::TComponent(component)) = tag
{
ref_at_cursor = Some(("", offset_at_cursor..offset_at_cursor));
ref_kind = Some(RefKind::PropOf(component));
}
if ref_at_cursor.is_some() {
break;
}
if let Some(Tag::TComponent(..)) = &tag {
_ = tag.take();
}
}
Ok(Token::Comment { text, .. }) if text.trim_start().starts_with("@type") => {
let annotation = text.trim().strip_prefix("@type").unwrap();
let Some((identifier, model)) = annotation.trim().split_once(|c: char| c.is_ascii_whitespace())
else {
continue;
};
scope.insert(identifier.trim().to_string(), Type::Model(model.trim().into()));
}
Err(Error::InvalidAttribute(StreamError::InvalidChar(_, b'=', mut expected_eq_pos), start_pos)) => {
let Some(Tag::TComponent(component)) = tag else {
break;
};
expected_eq_pos.col = expected_eq_pos.col.saturating_sub(1);
let start_pos: ByteOffset = rope_conv(span_conv(start_pos), slice);
let expected_eq_pos: ByteOffset = rope_conv(span_conv(expected_eq_pos), slice);
let mut range = (start_pos..expected_eq_pos).erase();
if !range.contains_end(offset_at_cursor) {
break;
}
let Ok(slice) = slice.try_slice(range.clone()) else {
break;
};
let Cow::Borrowed(mut attr) = Cow::from(slice) else {
panic!("(gather_refs) doesn't support cross-chunked boundaries");
};
let trimmed = attr.trim_end_matches(|c: char| !c.is_ascii_alphanumeric());
if trimmed != attr {
attr = trimmed;
range = range.start..range.start + trimmed.len();
}
let trimmed = attr.trim_start_matches(|c: char| !c.is_ascii_alphanumeric());
if trimmed != attr {
range = range.start + (attr.len() - trimmed.len())..range.end;
attr = trimmed;
}
ref_at_cursor = Some((attr, range));
ref_kind = Some(RefKind::PropOf(component));
break;
}
Err(_) => break,
Ok(token) => {
if token_span(&token).start() > offset_at_cursor {
break;
}
}
}
}
let model_filter = if arch_mode {
if matches!(ref_kind, Some(RefKind::Id))
&& model_filter
.as_ref()
.map(|v| v.iter().any(|m| m.as_str() == "ir.actions.act_window"))
.unwrap_or(false)
{
model_filter
} else {
arch_model.map(|span| vec![ImStr::from(span.as_str())]).or(model_filter)
}
} else {
model_filter
};
Ok(XmlRefs {
ref_at_cursor,
ref_kind,
model_filter,
scope,
arch_model: arch_model.map(|arch_model| ImStr::from(arch_model.as_str())),
})
}
fn insert_in_scope(
&self,
scope: &mut Scope,
identifier: &str,
mut root: tree_sitter::Node,
contents: &str,
as_iteratee: bool,
) -> anyhow::Result<()> {
normalize(&mut root);
let (type_, tid) = match self.type_of(root, scope, contents) {
Some(tid) => (type_cache().resolve(tid), tid),
None => (&Type::Value, type_cache().get_or_intern(Type::Value)),
};
let mut tid = self
.try_resolve_model(type_, scope)
.map(|model| type_cache().get_or_intern(Type::Model(_R(model).into())))
.unwrap_or(tid);
if as_iteratee && let Some(iter_tid) = self.type_of_iterable(tid) {
tid = iter_tid;
}
scope.insert(identifier.to_string(), type_cache().resolve(tid).clone());
Ok(())
}
}
pub fn add_xml_snippets(res: Option<CompletionResponse>) -> CompletionResponse {
let mut res = res.unwrap_or_else(|| CompletionResponse::List(Default::default()));
let list = match &mut res {
CompletionResponse::List(CompletionList { items, .. }) => items,
CompletionResponse::Array(items) => items,
};
list.extend([
CompletionItem {
kind: Some(CompletionItemKind::SNIPPET),
label: "view-inherit".to_string(),
insert_text: Some(VIEW_INHERIT_SNIPPET.to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
kind: Some(CompletionItemKind::SNIPPET),
label: "field".to_string(),
insert_text: Some(r#"<field name="$1"></field>"#.to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
kind: Some(CompletionItemKind::SNIPPET),
label: "xpath".to_string(),
insert_text: Some(
r#"<xpath expr="${1://div[hasclass('sample')]}" position="${2:inside}">$0</xpath>"#.to_string(),
),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
kind: Some(CompletionItemKind::SNIPPET),
label: "groupby".to_string(),
insert_text: Some(
r#"<filter name="group_by_$1" string="$2" domain="[]" context="{'group_by': '$1'}"/>"#.to_string(),
),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
kind: Some(CompletionItemKind::SNIPPET),
label: "res_groups".to_string(),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(
r#"
<record id="group_$1" model="res.groups">
<field name="name">$1</field>
<field name="privilege_id" ref="$2"/>
<field name="implied_ids" eval="[$3]"/>
<field name="comment">$4</field>
</record>"#
.to_string(),
),
..Default::default()
},
]);
res
}
#[derive(Debug)]
struct XmlRefs<'a> {
ref_at_cursor: Option<(&'a str, core::ops::Range<usize>)>,
ref_kind: Option<RefKind<'a>>,
model_filter: Option<Vec<ImStr>>,
scope: Scope,
arch_model: Option<ImStr>,
}
#[inline]
fn determine_csv_xmlid_subgroup_of_xmlspan<'text>(
ref_at_cursor: &mut Option<(&'text str, core::ops::Range<usize>)>,
value: StrSpan<'text>,
offset_at_cursor: usize,
) {
determine_csv_xmlid_subgroup(ref_at_cursor, (value.as_str(), value.range()), offset_at_cursor);
}
pub(crate) fn determine_csv_xmlid_subgroup<'text>(
ref_at_cursor: &mut Option<(&'text str, core::ops::Range<usize>)>,
value: (&'text str, core::ops::Range<usize>),
offset_at_cursor: usize,
) {
let (value, range) = value;
let mut start = range.start;
let mut csv_groups = value;
if !csv_groups.contains(',') {
*ref_at_cursor = Some((csv_groups, range));
return;
}
loop {
let mut last_subgroup = false;
let subgroup = match csv_groups.split_once(',') {
Some((subgroup, rest)) => {
csv_groups = rest;
subgroup
}
None => {
last_subgroup = true;
csv_groups
}
};
let range = start..start + subgroup.len();
if range.contains_end(offset_at_cursor) {
*ref_at_cursor = Some((subgroup, range));
break;
}
start += subgroup.len() + 1;
if last_subgroup {
break;
}
}
}
struct AttrPair<'a> {
lhs: &'a str,
rhs: &'a str,
lhs_val: Option<(ImStr, usize)>,
rhs_val: Option<(ImStr, usize)>,
}
fn attr_pair<'a>(lhs: &'a str, rhs: &'a str) -> AttrPair<'a> {
AttrPair {
lhs,
rhs,
lhs_val: None,
rhs_val: None,
}
}
impl AttrPair<'_> {
#[must_use]
fn accept(&mut self, local: &str, value: StrSpan) -> Option<((ImStr, usize), (ImStr, usize))> {
if local == self.lhs {
self.lhs_val = Some((value.as_str().into(), value.start()));
} else if local == self.rhs {
self.rhs_val = Some((value.as_str().into(), value.start()));
}
if let Some((lhs, lhs_start)) = &self.lhs_val
&& let Some((rhs, rhs_start)) = &self.rhs_val
{
let res = Some(((lhs.clone(), *lhs_start), (rhs.clone(), *rhs_start)));
self.reset();
return res;
}
None
}
fn reset(&mut self) {
self.lhs_val = None;
self.rhs_val = None;
}
}