Search code examples
kotlinintellij-ideaintellij-plugincode-completion

Implementing IntelliJ Completion Contributor for the file top level


I'm developing a custom language plugin for IntelliJ and I want to add code completion support using the CompletionContributor. The language I'm developing support for IntelliJ uses OOP, and provides the ability to use typical classes (class) and namespaces (namespace).

At the moment, everything is extremely clear except for one thing. I can’t understand how to call specific completion provider only at the highest level of the file scope. Below I give an example to show clearly the place where autocomplete is needed at the moment (pseudocode):

1. namespace Foo;
2.
3. class Test {
4. 
5. }
6.
7. function foo() {
8. 
9. }

In the above example, the completion provider should be used only on lines 1 and 2 (class scope), partially on line 3 (up to curly brace), as well as on line 6. In short, completion provider shouldn't be invoked for line 4 and 8.

Please note the file may be empty:

1.
2.

In this case, the code completion should work too.

Bellow is the boilerplate code to achieve this (Kotlin).

Contributor:

// com.some.lang.core.completion.MyCompletionContributor

package com.some.lang.core.completion

import com.intellij.codeInsight.completion.CompletionContributor
import com.some.lang.core.completion.providers.FileScopeCompletionProvider

class MyCompletionContributor : CompletionContributor() {
    private val providers = listOf(
        FileScopeCompletionProvider
    )

    init {
        providers.forEach { extend(it) }
    }

    private fun extend(provider: MyCompletionProvider) {
        extend(provider.type, provider.context, provider)
    }
}

Abstract Provider:

// package com.some.lang.core.completion.MyCompletionProvider

package com.some.lang.core.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.patterns.ElementPattern
import com.intellij.psi.PsiElement

abstract class MyCompletionProvider : CompletionProvider<CompletionParameters>() {
    abstract val context: ElementPattern<out PsiElement>
    open val type: CompletionType = CompletionType.BASIC
}

File Scope Provider:

// package com.some.lang.core.completion.providers.FileScopeCompletionProvider

package com.some.lang.core.completion.providers

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.patterns.ElementPattern
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
import com.some.lang.core.Language
import com.some.lang.core.completion.MyCompletionProvider

object FileScopeCompletionProvider : MyCompletionProvider() {
    override val context: ElementPattern<PsiElement>
        get() = PlatformPatterns.psiElement().withLanguage(Language)

    override fun addCompletions(
        parameters: CompletionParameters,
        processingContext: ProcessingContext,
        result: CompletionResultSet
    ) {
        result.addElement(LookupElementBuilder.create("Hello"))
    }
}

Of course, this code doesn't do what is needed. However, it does show the general design that I use. I am sure that I need to fix the following lines:

    override val context: ElementPattern<PsiElement>
        get() = PlatformPatterns.psiElement().withLanguage(Language)

And the main question is that I don’t understand how to do it.

Update:

Relevant BNF part:

{
    psiClassPrefix='My'

    // ...
}

File ::= TopStatement*

private TopStatement ::= NamespaceStatement (ClassDefinition | InterfaceDefinition)

NamespaceStatement ::= 'namespace' ComplexId ';' {pin=2}

ClassDefinition ::= ClassModifier? 'class' Id SuperClass? ImplementsList? ClassBody {pin=3}

// ...

Solution

  • You can use with to add your own PatternCondition to your element patterns.

    Assuming you have an isTopLevel function defined something like this:

    fun isTopLevel(elem: PsiElement): Boolean = elem.parent is MyLanguageFile
    

    You can use this ElementPattern to make your completion only available to top-level elements.

    val context = PlatformPatterns
      .psiElement()
      .with(object : PatternCondition<PsiElement>("toplevel") {
        override fun accepts(elem: PsiElement, context: ProcessingContext?) = isTopLevel(elem)
      })
    

    Edit: You can also use withElementType to control which element types the completion will apply to. For example:

    context = psiElement()
      .andOr(
        psiElement().withElementType(NAMESPACE_NAME),
        psiElement().withElementType(CLASS_NAME),
        //other top level stuff
      )