JetBrains / js-graphql-intellij-plugin

GraphQL language support for WebStorm, IntelliJ IDEA and other IDEs based on the IntelliJ Platform.
https://jimkyndemeyer.github.io/js-graphql-intellij-plugin/
MIT License
880 stars 97 forks source link

Add extension point to support indexing/usage support in other languages' string literals #628

Closed theY4Kman closed 1 year ago

theY4Kman commented 1 year ago

It would be nice if there were an extension point enabling other plugins to support GQL-injected string literals in other languages.

Currently, informing the indexer to include a language's FileTypes can be accomplished with a GraphQLFindUsagesFileTypeContributor extension. However, only the elements allowed by GraphQLInjectionSearchHelper.isGraphQLLanguageInjectionTarget may be included in indexing, so the GraphQLJavaScriptInjectionSearchHelper service must be overridden. This gets indexing down to the language's injection host element... but the extraction of the GQL source is performed using PsiElement.getText(), which will also contain the string literal's bounding chars (e.g., quotes, or backticks). At the moment, this is rolled over for JavaScript by stripping whitespace and backticks, but this will not work for all languages.


I've built out a working implementation branch, with the extension interface declared as:

public interface GraphQLInjectionHelperExtension {
    /**
     * Gets whether the specified host is a target for GraphQL Injection
     */
    boolean isGraphQLLanguageInjectionTarget(PsiElement host);

    /**
     * Extract GraphQL source from a GraphQL Injection target and create a corresponding GraphQL PsiFile.
     * This will only be called if {@link #isGraphQLLanguageInjectionTarget} returns true for the host.
     *
     * @param host the target for GraphQL Injection
     * @return a GraphQL PsiFile generated from the extracted GraphQL source
     */
    default @Nullable GraphQLFile createGraphQLFileFromInjectionTarget(PsiElement host) {
        final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(host.getProject());
        final String graphQLSource = extractGraphQLSourceFromInjectionTarget(host);
        if (graphQLSource == null) {
            return null;
        }

        final PsiFile graphqlInjectedPsiFile = psiFileFactory.createFileFromText("", GraphQLFileType.INSTANCE, graphQLSource, 0, false, false);
        return (GraphQLFile) graphqlInjectedPsiFile;
    }

    /**
     * Extract GraphQL source from a GraphQL Injection target.
     * This will only be called if {@link #isGraphQLLanguageInjectionTarget} returns true for the host.
     *
     * @param host the target for GraphQL Injection
     * @return the GraphQL source extracted from host
     */
    @Nullable String extractGraphQLSourceFromInjectionTarget(PsiElement host);
}

Personally, I wanted to add support for Python. I implemented an extension in a plugin like so:

class GraphQLPythonInjectionHelperExtensions : GraphQLInjectionHelperExtension {
    override fun isGraphQLLanguageInjectionTarget(host: PsiElement): Boolean {
        if (host !is PyStringLiteralExpression) return false
        return pyModuleFunctionArgument("gql", 0, "gql").accepts(host)
    }

    override fun extractGraphQLSourceFromInjectionTarget(host: PsiElement): String? =
        host.asSafely<PyStringLiteralExpression>()?.stringValue
}

(Though, it should be noted isGraphQLLanguageInjectionTarget should only rely on what's available in the passed element's containing file, and pyModuleFunctionArgument will lodge an error in the IDE. It's just a proof-of-concept.)

If this seems useful to others, I can open a pull request.

theY4Kman commented 1 year ago

With the introduction of the fileTypeContributor and injectedLanguage extension points in 4.0.0, this is now satisfied (with much better and more concise naming, too)

theY4Kman commented 1 year ago

For reference, here is what my extension implementations look like for Python:

plugin.xml ```xml ```
PythonGraphQLFileTypeContributor.kt ```kt package my.pkg.ide.injection import com.intellij.lang.jsgraphql.ide.injection.GraphQLFileTypeContributor import com.intellij.openapi.fileTypes.FileType import com.jetbrains.python.PythonFileType class PythonGraphQLFileTypeContributor : GraphQLFileTypeContributor { override fun getFileTypes(): Collection = listOf(PythonFileType.INSTANCE) } ```
PythonGraphQLInjectedLanguage.kt ```kt package my.pkg.ide.injection import com.intellij.lang.jsgraphql.ide.injection.GraphQLInjectedLanguage import com.intellij.psi.PsiElement import com.intellij.util.asSafely import com.jetbrains.python.patterns.PythonPatterns.pyModuleFunctionArgument import com.jetbrains.python.psi.PyElement import com.jetbrains.python.psi.PyStringLiteralExpression class PythonGraphQLInjectedLanguage : GraphQLInjectedLanguage { override fun accepts(host: PsiElement): Boolean = host is PyElement override fun escapeHostElements(rawText: String?): String? { // TODO(zk): Implement this return rawText } override fun getInjectedTextForIndexing(host: PsiElement): String? = host.asSafely()?.stringValue override fun isLanguageInjectionTarget(host: PsiElement?): Boolean { if (host !is PyStringLiteralExpression) return false return pyModuleFunctionArgument("gql", 0, "gql").accepts(host) } } ```