Xtext Content Assist: Escape identifiers conflicting with keywords

I have faced in Spray the situation that a segment of a package name of a qualified Java type conflicts with a keyword of the Spray language. Specifically you can refer to custom features with the custom keyword, and the referred type was in a package containing also the name custom.

When inserting the proposed qualified name this results in an error due to the keyword collision.

The qualified name is usually combined through a sequence of ID rules, which allows escaping an identifier by prefixing it with the ^ character in the case of conflict with a keyword.

For content assist it would now be useful if the inserted string would be automatically escaped. So here is a way to solve this.

My first approach was to override the completeJvmParameterizedTypeReference_Type() method from the proposal provider and modify the proposals through a custom ICompletionProposalAcceptor. Sebastian Zarnekow commented that the proper way would be to register a IValueConverter for QualifiedName. I tried to do so, but was not successful. Actually there is already an QualifiedNameValueConverter registered.

My next solution is now to subclass JdtTypesProposalProvider and pass a custom IValueConverter. The overridden methods just pass null as converter, thus do no conversion. I am reusing the QualifiedNameValueConverter here, since it does the necessary keyword escaping.

public class SprayJdtTypesProposalProvider extends JdtTypesProposalProvider {
    @Inject
    private QualifiedNameValueConverter qnValueConverter;

    /**
     * Overridden to pass a default value converter
     */
    @Override
    public void createSubTypeProposals(JvmType superType, ICompletionProposalFactory proposalFactory, ContentAssistContext context, EReference typeReference, Filter filter, ICompletionProposalAcceptor acceptor) {
        createSubTypeProposals(superType, proposalFactory, context, typeReference, filter, getConverter(), acceptor);
    }

    /**
     * Overridden to pass a default value converter
     */
    @Override
    public void createTypeProposals(ICompletionProposalFactory proposalFactory, ContentAssistContext context, EReference typeReference, Filter filter, ICompletionProposalAcceptor acceptor) {
        createTypeProposals(proposalFactory, context, typeReference, filter, getConverter(), acceptor);
    }

    private AbstractValueConverter<String> getConverter() {
        return new AbstractValueConverter<String>() {
            /**
             * Remove ^ character from escaped segments
             */
            @Override
            public String toValue(String string, INode node) throws ValueConverterException {
                return string.replace("^", "");
            }

            /**
             * Escape segments colliding with keywords with ^ character
             */
            @Override
            public String toString(String value) throws ValueConverterException {
                // this converter will escape keywords with ^
                return qnValueConverter.toString(value);
            }
        };
    }
}

This custom implementation must be bound to the UI module:

public class SprayUiModule extends AbstractSprayUiModule {
    @Override
    public Class<? extends ITypesProposalProvider> bindITypesProposalProvider() {
        return SprayJdtTypesProposalProvider.class;
    }
}

Now the inserted qualified type name will be automatically replaced.

How to limit proposed Java types to implementors of an interface

When you develop a DSL that derives from Xbase you likely want to refer to Jvm types through a JvmTypeReference.

CustomBehavior :
    'custom' (name = ID | 'ref' realizedBy=JvmTypeReference) (label = STRING);

If you hit CTRL+SPACE at such a position in the editor, by default you will get all visible Java types proposed:

This is not really useful when it only makes sense to refer to specifc types. If you want to limit the proposals to just implementors of a specific interface, you can customize this in the proposal provider by overriding the method completeJvmParameterizedTypeReference_Type(). The following example from Spray shows limiting the types to implementors of Graphiti’s ICustomFeature interface in the context of Spray’s CustomBehavior:

public class SprayProposalProvider extends AbstractSprayProposalProvider {
    @Inject
    private IJvmTypeProvider.Factory      jvmTypeProviderFactory;
    @Inject
    private ITypesProposalProvider        typeProposalProvider;
    
    @Override
    public void completeJvmParameterizedTypeReference_Type(EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor) {
        if (EcoreUtil2.getContainerOfType(model, CustomBehavior.class) != null) {
            final IJvmTypeProvider jvmTypeProvider = jvmTypeProviderFactory.createTypeProvider(model.eResource().getResourceSet());
            // Graphiti specific
            final JvmType interfaceToImplement = jvmTypeProvider.findTypeByName(ICustomFeature.class.getName());
            typeProposalProvider.createSubTypeProposals(interfaceToImplement, this, context, SprayPackage.Literals.BEHAVIOR__REALIZED_BY, TypeMatchFilters.canInstantiate(), acceptor);
        } else {
            super.completeJvmParameterizedTypeReference_Type(model, assignment, context, acceptor);
        }
    }

Now you will get the desired subtypes as proposals:

Note that this does only limit proposals to certain types. It does not prevent you from actually referring to another type that you don’t get proposed. This needs to be restricted in your scope provider.