Poly-reference issues via XML DOM API in multi-module project

Answered

Hello!

What is a preferred way to implement a poly-reference (PsiPolyVariantReference) via CustomReferenceConverter (XML DOM Api)?

Consider an example. Let multi-module project (module A and B), each have the foo.xml file:

A/foo.xml:

<foo>
<bar name="AAA"/>

<qux ref="AAA"/>
<qux ref="BBB"/>
</foo>

B/foo.xml:

<foo>
<bar name="AAA"/>
<bar name="BBB"/>

<qux ref="AAA"/>
</foo>

Element bar with name BBB has a single declaration in a file from module B and single usage in element qux in a file from module A. While, element bar with name AAA has multiple valid declarations in both modules.

In such case, auto resolving via DomResolveConverter wouldn't work (Since, internaly it uses a Map<String, DomElement> which is suitable for a single element with a particular name, so even implementation of MergingFileDescription or usage of ModelMerger are not usefull).

Looks like CustomReferenceConverter is a solution, but what exactly have to be returned from PsiPolyVariantReference.multiResolve method? An XmlAttributeValue, DomTarget wrapped in PomTargetPsiElement, etc.?

After some experementations, turns out that:

1. In both approaches, referencing and highlighting of AAA elements works fine.

However, find usages shows results only from an invoked file.

2. With PomTargetPsiElement approach, BBB element resolving is ok, but reference (at the cursor) is not highlighted and find usages shows only declaration.

3. With XmlAttributeValue approach, find usages works as expected and reference (at the cursor) is highlighted, but highlighting color is a color of write access instead of read access.

 

So, what is a proprer way to achive that?

 

P.S. By the way, are there any way to create partial merged model via ModelMerger? To merge not the full files, but only bar elements for instance..

Thanks in advance!

Regards,

Vadym

 

-----------------------------------------%<--------------------------------------

Source code:

@DefinesXml
public interface Foo extends DomElement {
@SubTagList("bar")
List<Bar> getBars();

@SubTagList("qux")
List<Qux> getQuxies();

default @Nullable Bar findBar(@NotNull String name) {
for (Bar bar : getBars()) {
if (name.equals(ElementPresentationManager.getElementName(bar))) {
return bar;
}
}
return null;
}
}
public interface Bar extends DomElement {
@Attribute("name")
@NameValue
@Required
@Referencing(BarCustomReferenceConverter.class)
GenericAttributeValue<String> getName();
}
public interface Qux extends DomElement {
@Attribute("ref")
@Required
@Referencing(BarCustomReferenceConverter.class)
GenericAttributeValue<String> getRef();
}
public class FooDomFileDescription extends DomFileDescription<Foo> {
public FooDomFileDescription() {
super(Foo.class, "foo");
}

@Override
public boolean isMyFile(@NotNull XmlFile file, @Nullable Module module) {
return file.getName().equals("foo.xml");
}

/** @apiNote No caching for the sake of clarity */
public static boolean processFooFiles(@NotNull Project project, @NotNull GlobalSearchScope scope,
@NotNull Processor<Foo> processor) {
final var manager = DomManager.getDomManager(project);
return FilenameIndex.processFilesByName("foo.xml", false, true,
it -> {
final var file = manager.getFileElement((XmlFile) it, Foo.class);
return file == null || processor.process(file.getRootElement());
}, scope, project
);
}
}
public class BarCustomReferenceConverter implements CustomReferenceConverter<String> {
@Override
public @NotNull PsiReference[] createReferences(GenericDomValue<String> value, PsiElement element, ConvertContext context) {
return new PsiReference[]{new BarReference((XmlAttributeValue) element)};
}

public static class BarReference extends PsiPolyVariantReferenceBase<XmlAttributeValue> {
public BarReference(@NotNull XmlAttributeValue element) {
super(element);
}

/** @apiNote No caching for the sake of clarity */
@Override
public @NotNull ResolveResult[] multiResolve(boolean incompleteCode) {
final String nameToSearch = getValue();
final var project = myElement.getProject();
final var scope = myElement.getResolveScope();
final var results = new SmartList<PsiElement>();

FooDomFileDescription.processFooFiles(project, scope,
foo -> {
final Bar bar = foo.findBar(nameToSearch);
if (bar == null) return true;

final PsiElement target = getTarget(bar);
assert target != null;
results.add(target);
return true;
}
);

return PsiElementResolveResult.createResults(results);
}

// /** Approach #1: */
// private static @Nullable PsiElement getTarget(@NotNull Bar bar) {
// return bar.getName().getXmlAttributeValue();
// }

/** Approach #2: */
private static @Nullable PsiElement getTarget(@NotNull Bar bar) {
final var target = DomTarget.getTarget(bar);
return target == null ? null : PomService.convertToPsi(target);
}
}
}
0
1 comment

ModelMerger approach is most probably not the solution for this case, it's really used if you need to "join" all corresponding files into one big set (affects "everything" in DOM).

DomTarget approach should work - why do you have @Referencing(BarCustomReferenceConverter.class) in Bar#getName()? AFAIU it is the declaration and not a reference to anything?

0

Please sign in to leave a comment.