Repository: apollographql/apollo-kotlin
Pull request #4886 (2023-04-20 11:31:02)

[IJ plugin] Implement the GraphQL plugin's configContributor extension point
Implements #4625 

Can be tested with [this sample project](https://github.com/BoD/apollo-kotlin-template-multi-modules-and-services/tree/apollo-kotlin-v4).


Patch:
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 5fa6e3f9a8..b8668d1e22 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -19,6 +19,7 @@ golatac.init(file("../gradle/libraries.toml"))
 
 dependencies {
   compileOnly(gradleApi())
+  compileOnly(golatac.lib("gegp"))
 
   implementation(golatac.lib("okhttp"))
 
diff --git a/gradle/libraries.toml b/gradle/libraries.toml
index 8e08c8ba58..995c92a235 100644
--- a/gradle/libraries.toml
+++ b/gradle/libraries.toml
@@ -75,6 +75,7 @@ clikt = { group = "com.github.ajalt.clikt", name = "clikt", version = "3.5.2" }
 compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version = "1.3.3" }
 dokka-base = { group = "org.jetbrains.dokka", name = "dokka-base", version.ref = "dokka" }
 dokka-plugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" }
+gegp = "com.gradle:gradle-enterprise-gradle-plugin:3.13" # // sync with settings.gradle.kts
 # Not updating because it fails in apollo-compiler Java tests with
 # annotation @org.jetbrains.annotations.Nullable not applicable in this type context
 google-testing-compile = { group = "com.google.testing.compile", name = "compile-testing", version = "0.19" }
@@ -91,7 +92,7 @@ gradle-japicmp-plugin = { group = "me.champeau.gradle", name = "japicmp-gradle-p
 gradle-publish-plugin = { group = "com.gradle.publish", name = "plugin-publish-plugin", version = "1.1.0" }
 graphqlkotlin = { group = "com.expediagroup", name = "graphql-kotlin-spring-server", version = "5.3.0" }
 guava-jre = { group = "com.google.guava", name = "guava", version.ref = "guava" }
-intellij-plugin = "org.jetbrains.intellij.plugins:gradle-intellij-plugin:1.13.1"
+intellij-plugin = "org.jetbrains.intellij.plugins:gradle-intellij-plugin:1.13.3"
 intellij-changelog = "org.jetbrains.intellij.plugins:gradle-changelog-plugin:2.0.0"
 jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
diff --git a/gradle/repositories.gradle.kts b/gradle/repositories.gradle.kts
index 8f91177fba..85b98cc752 100644
--- a/gradle/repositories.gradle.kts
+++ b/gradle/repositories.gradle.kts
@@ -21,6 +21,7 @@ listOf(pluginManagement.repositories, dependencyResolutionManagement.repositorie
         includeModule("me.champeau.gradle", "japicmp-gradle-plugin")
         includeModule("com.gradle.publish", "plugin-publish-plugin")
         includeModule("com.github.ben-manes", "gradle-versions-plugin")
+        includeModule("com.gradle", "gradle-enterprise-gradle-plugin")
 
         // For org.jetbrains.intellij
         includeModule("org.jetbrains.intellij", "org.jetbrains.intellij.gradle.plugin")
diff --git a/intellij-plugin/README.md b/intellij-plugin/README.md
index 6b6c935e55..48b468c64b 100644
--- a/intellij-plugin/README.md
+++ b/intellij-plugin/README.md
@@ -2,7 +2,6 @@
 
 > Work in progress!
 
-![Build](https://github.com/apollographql/apollo-intellij-plugin/workflows/Build/badge.svg)
 [![Version](https://img.shields.io/jetbrains/plugin/v/20645.svg)](https://plugins.jetbrains.com/plugin/20645)
 [![Downloads](https://img.shields.io/jetbrains/plugin/d/20645.svg)](https://plugins.jetbrains.com/plugin/20645)
 
@@ -13,21 +12,29 @@ This plugin for Android Studio and IntelliJ helps you work with the
 
 Note: this plugin is still in early development and is not yet ready for production use.
 
+### Features
+- Automatic code generation: models are re-generated whenever GraphQL files change
+- Integration with the [GraphQL IntelliJ Plugin](https://plugins.jetbrains.com/plugin/8097-js-graphql): the structure of the Apollo project is automatically contributed, so there is no need to create a `graphql.config.yml` / `.graphqlconfig` file
+- Migration helpers:
+  - Apollo Android 2.x → Apollo Kotlin 3.x
+  - Apollo Kotlin 3.x → Apollo Kotlin 4.x (WIP)
+  - `compat` codegen → `operationBased` codegen
+- More to come!
+
 <!-- Plugin description end -->
 
 ## Installation
 
-As time of writing, the plugin is supported by:
+The plugin is supported on:
 
-* IntelliJ 2022.2+
-* Android Studio Preview (Flamingo)
-* Android Studio Canary (Giraffe)
+* IntelliJ 2022.2 and above
+* Android Studio 2022.2 (Flamingo) and above
 
-Android Studio Stable (Electric Eel) is not supported
+#### Installation instructions:
 
-<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > ⚙️ > <kbd>Manage Plugin
+<kbd>Settings</kbd> > <kbd>Plugins</kbd> > <kbd>⚙</kbd>️ > <kbd>Manage Plugin
 Repositories</kbd> > <kbd>+</kbd> > <kbd>https://raw.githubusercontent.com/apollographql/apollo-kotlin/main/intellij-plugin/snapshots/plugins.xml</kbd>
 
 Then:
 
-<kbd>Marketplace</kbd> > <kbd>Search for "Apollo GraphQL"</kbd> > <kbd>Install Plugin</kbd>
+<kbd>Marketplace</kbd> > <kbd>Search for "Apollo GraphQL"</kbd> > <kbd>Install</kbd>
diff --git a/intellij-plugin/gradle.properties b/intellij-plugin/gradle.properties
index 6827c85663..09a1ddcf51 100644
--- a/intellij-plugin/gradle.properties
+++ b/intellij-plugin/gradle.properties
@@ -15,6 +15,7 @@ pluginUntilBuild=231.*
 # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
 platformType=IC
 # Corresponds to AS Flamingo 2022.2.1 Canary 1 -> https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html
+# and https://developer.android.com/studio/archive (more up to date)
 # See also https://plugins.jetbrains.com/docs/intellij/android-studio.html
 platformVersion=222.3739.54
 
@@ -22,7 +23,8 @@ platformVersion=222.3739.54
 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
 # To find the version of a plugin relative to the platoform version, see the plugin's page on the Marketplace,
 # e.g. for the toml plugin: https://plugins.jetbrains.com/plugin/8195-toml/versions/stable
-platformPlugins=com.intellij.java, org.jetbrains.kotlin, com.intellij.gradle, com.intellij.lang.jsgraphql:3.4.0, org.toml.lang:222.3739.16
+# Note: jsgraphql has 2 published versions: one with `pluginSinceBuild` set to latest, and one set to an older (more broadly available) version.
+platformPlugins=com.intellij.java, org.jetbrains.kotlin, com.intellij.gradle, com.intellij.lang.jsgraphql:4.0.0-222, org.toml.lang:222.3739.16
 
 # JVM language level used to build project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+.
 javaVersion=17
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
index d28e64e316..2c4fe3e755 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
@@ -1,16 +1,28 @@
 package com.apollographql.ijplugin.gradle
 
 import com.apollographql.apollo3.gradle.api.ApolloGradleToolingModel
+import com.apollographql.ijplugin.ApolloBundle
+import com.apollographql.ijplugin.graphql.GraphQLProjectFiles
+import com.apollographql.ijplugin.graphql.GraphQLProjectFilesListener
+import com.apollographql.ijplugin.project.ApolloProjectListener
 import com.apollographql.ijplugin.project.apolloProjectService
+import com.apollographql.ijplugin.util.dispose
+import com.apollographql.ijplugin.util.isNotDisposed
 import com.apollographql.ijplugin.util.logd
 import com.apollographql.ijplugin.util.logw
+import com.apollographql.ijplugin.util.newDisposable
 import com.intellij.openapi.Disposable
 import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
 import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter
 import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType
 import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
+import com.intellij.openapi.progress.ProcessCanceledException
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.util.CheckedDisposable
 import com.intellij.openapi.vfs.VfsUtilCore
 import com.intellij.openapi.vfs.VirtualFileManager
 import org.gradle.tooling.CancellationTokenSource
@@ -21,34 +33,76 @@ import org.jetbrains.plugins.gradle.service.execution.GradleExecutionHelper
 import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings
 import org.jetbrains.plugins.gradle.util.GradleConstants
 import java.io.File
-import java.util.concurrent.Executors
 
 class GradleToolingModelService(
     private val project: Project,
 ) : Disposable {
+  private var gradleHasSyncedDisposable: CheckedDisposable? = null
+
   private var gradleCancellation: CancellationTokenSource? = null
   private var abortRequested: Boolean = false
-  private val gradleExecutorService = Executors.newSingleThreadExecutor()
+
+  var graphQLProjectFiles: List<GraphQLProjectFiles> = emptyList()
 
   init {
     logd("project=${project.name}")
-    startObserveGradleHasSynced()
+    startObserveApolloProject()
+    startOrStopObserveGradleHasSynced()
+    startOrAbortFetchToolingModels()
+  }
+
+  private fun startObserveApolloProject() {
+    logd()
+    project.messageBus.connect(this).subscribe(ApolloProjectListener.TOPIC, object : ApolloProjectListener {
+      override fun apolloProjectChanged(isApolloAndroid2Project: Boolean, isApolloKotlin3Project: Boolean) {
+        logd("isApolloAndroid2Project=$isApolloAndroid2Project isApolloKotlin3Project=$isApolloKotlin3Project")
+        startOrStopObserveGradleHasSynced()
+        startOrAbortFetchToolingModels()
+      }
+    })
+  }
+
+  private fun shouldFetchToolingModels() = project.apolloProjectService.isApolloKotlin3Project
+
+  private fun startOrStopObserveGradleHasSynced() {
+    logd()
+    if (shouldFetchToolingModels()) {
+      startObserveGradleHasSynced()
+    } else {
+      stopObserveGradleHasSynced()
+    }
   }
 
   private fun startObserveGradleHasSynced() {
     logd()
-    project.messageBus.connect(this).subscribe(GradleHasSyncedListener.TOPIC, object : GradleHasSyncedListener {
+    if (gradleHasSyncedDisposable.isNotDisposed()) {
+      logd("Already observing")
+      return
+    }
+    val disposable = newDisposable()
+    gradleHasSyncedDisposable = disposable
+    project.messageBus.connect(disposable).subscribe(GradleHasSyncedListener.TOPIC, object : GradleHasSyncedListener {
       override fun gradleHasSynced() {
         logd()
-        abortFetchToolingModels()
-
-        if (project.apolloProjectService.isApolloKotlin3Project) {
-          fetchToolingModels()
-        }
+        startOrAbortFetchToolingModels()
       }
     })
   }
 
+  private fun stopObserveGradleHasSynced() {
+    logd()
+    dispose(gradleHasSyncedDisposable)
+    gradleHasSyncedDisposable = null
+  }
+
+  private fun startOrAbortFetchToolingModels() {
+    logd()
+    abortFetchToolingModels()
+    if (shouldFetchToolingModels()) {
+      fetchToolingModels()
+    }
+  }
+
   private fun fetchToolingModels() {
     logd()
 
@@ -57,8 +111,17 @@ class GradleToolingModelService(
       return
     }
 
-    val rootProjectPath = project.getGradleRootPath() ?: return
-    gradleExecutorService.submit {
+    FetchToolingModelsTask().queue()
+  }
+
+  private inner class FetchToolingModelsTask : Task.Backgroundable(
+      project,
+      @Suppress("DialogTitleCapitalization")
+      ApolloBundle.message("GradleToolingModelService.fetchToolingModels.progress"),
+      true,
+  ) {
+    override fun run(indicator: ProgressIndicator) {
+      val rootProjectPath = project.getGradleRootPath() ?: return
       abortRequested = false
       val gradleExecutionHelper = GradleExecutionHelper()
       val executionSettings = ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>(project, rootProjectPath, GradleConstants.SYSTEM_ID)
@@ -76,17 +139,26 @@ class GradleToolingModelService(
         } finally {
           gradleCancellation = null
         }
-      } ?: return@submit
+      } ?: return
 
       // We're only interested in projects that apply the Apollo plugin - and thus have the codegen task registered
       val allApolloGradleProjects: List<GradleProject> = (rootGradleProject.children + rootGradleProject)
-          .filter { it.tasks.any { it.name == CODEGEN_GRADLE_TASK_NAME } }
+          .filter { gradleProject -> gradleProject.tasks.any { task -> task.name == CODEGEN_GRADLE_TASK_NAME } }
       logd("allApolloGradleProjects=${allApolloGradleProjects.map { it.name }}")
-      val allToolingModels = allApolloGradleProjects.mapNotNull { gradleProject ->
+      indicator.isIndeterminate = false
+      val allToolingModels = allApolloGradleProjects.mapIndexedNotNull { index, gradleProject ->
         if (abortRequested) {
           logd("Aborted")
-          return@submit
+          return@run
         }
+        try {
+          ProgressManager.checkCanceled()
+        } catch (e: ProcessCanceledException) {
+          logd("Canceled by user")
+          return@run
+        }
+
+        indicator.fraction = (index + 1).toDouble() / allApolloGradleProjects.size
         gradleExecutionHelper.execute(gradleProject.projectDirectory.canonicalPath, executionSettings) { connection ->
           gradleCancellation = GradleConnector.newCancellationTokenSource()
           logd("Fetch tooling model for :${gradleProject.name}")
@@ -107,38 +179,51 @@ class GradleToolingModelService(
       logd("allToolingModels=$allToolingModels")
       computeGraphQLProjectFiles(allToolingModels)
     }
+
+    override fun onCancel() {
+      logd()
+      abortFetchToolingModels()
+    }
   }
 
   private fun computeGraphQLProjectFiles(toolingModels: List<ApolloGradleToolingModel>) {
-    val projectServiceToDirs = mutableMapOf<String, Set<File>>()
-    fun getAllDirsForProjectService(projectName: String, serviceName: String): Set<File> {
+    // Compute the GraphQLProjectFiles, taking into account the dependencies between projects
+    val projectServiceToGraphQLProjectFiles = mutableMapOf<String, GraphQLProjectFiles>()
+    fun getGraphQLProjectFiles(projectName: String, serviceName: String): GraphQLProjectFiles {
       val key = "$projectName/$serviceName"
-      return projectServiceToDirs.getOrPut(key) {
+      return projectServiceToGraphQLProjectFiles.getOrPut(key) {
         val toolingModel = toolingModels.first { it.projectName == projectName }
         val serviceInfo = toolingModel.serviceInfos.first { it.name == serviceName }
-        serviceInfo.graphqlSrcDirs +
-            serviceInfo.schemaFiles +
-            serviceInfo.upstreamProjects.flatMap { getAllDirsForProjectService(it, serviceName) }
+        val dependenciesProjectFiles = serviceInfo.upstreamProjects.map { getGraphQLProjectFiles(it, serviceName) }
+        GraphQLProjectFiles(
+            name = key,
+            schemaPaths = (serviceInfo.schemaFiles.mapNotNull { it.toProjectLocalPathOrNull() } +
+                dependenciesProjectFiles.flatMap { it.schemaPaths })
+                .distinct(),
+            operationPaths = (serviceInfo.graphqlSrcDirs.mapNotNull { it.toProjectLocalPathOrNull() } +
+                dependenciesProjectFiles.flatMap { it.operationPaths })
+                .distinct(),
+        )
       }
     }
 
-    val projectDir = project.guessProjectDir() ?: return
     val graphQLProjectFiles = mutableListOf<GraphQLProjectFiles>()
     for (toolingModel in toolingModels) {
       for (serviceInfo in toolingModel.serviceInfos) {
-        graphQLProjectFiles += GraphQLProjectFiles(
-            name = "${toolingModel.projectName}/${serviceInfo.name}",
-            includedPaths = getAllDirsForProjectService(toolingModel.projectName, serviceInfo.name).mapNotNull {
-              val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(it.toPath()) ?: return@mapNotNull null
-              val relativePath = VfsUtilCore.getRelativeLocation(virtualFile, projectDir) ?: return@mapNotNull null
-              if (it.isDirectory) "$relativePath/*" else relativePath
-            }.toSet(),
-        )
+        graphQLProjectFiles += getGraphQLProjectFiles(toolingModel.projectName, serviceInfo.name)
       }
     }
-
-    // TODO expose this to the GraphQL plugin
+    this.graphQLProjectFiles = graphQLProjectFiles
     logd("graphQLProjectFiles=$graphQLProjectFiles")
+
+    // Project files are available, notify interested parties
+    project.messageBus.syncPublisher(GraphQLProjectFilesListener.TOPIC).projectFilesAvailable()
+  }
+
+  private fun File.toProjectLocalPathOrNull(): String? {
+    val projectDir = project.guessProjectDir() ?: return null
+    val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(toPath()) ?: return null
+    return VfsUtilCore.getRelativeLocation(virtualFile, projectDir)
   }
 
   private fun abortFetchToolingModels() {
@@ -151,11 +236,5 @@ class GradleToolingModelService(
   override fun dispose() {
     logd("project=${project.name}")
     abortFetchToolingModels()
-    gradleExecutorService.shutdown()
   }
 }
-
-data class GraphQLProjectFiles(
-    val name: String,
-    val includedPaths: Set<String>,
-)
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/ApolloGraphQLConfigContributor.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/ApolloGraphQLConfigContributor.kt
new file mode 100644
index 0000000000..24335903b7
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/ApolloGraphQLConfigContributor.kt
@@ -0,0 +1,39 @@
+package com.apollographql.ijplugin.graphql
+
+import com.apollographql.ijplugin.gradle.GradleToolingModelService
+import com.apollographql.ijplugin.project.apolloProjectService
+import com.apollographql.ijplugin.util.logd
+import com.intellij.lang.jsgraphql.ide.config.GraphQLConfigContributor
+import com.intellij.lang.jsgraphql.ide.config.loader.GraphQLRawConfig
+import com.intellij.lang.jsgraphql.ide.config.loader.GraphQLRawProjectConfig
+import com.intellij.lang.jsgraphql.ide.config.loader.GraphQLRawSchemaPointer
+import com.intellij.lang.jsgraphql.ide.config.model.GraphQLConfig
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+
+class ApolloGraphQLConfigContributor : GraphQLConfigContributor {
+  override fun contributeConfigs(project: Project): Collection<GraphQLConfig> {
+    logd()
+    val projectDir = project.guessProjectDir() ?: return emptyList()
+    // This can be called early, don't initialize services right away. It's ok because it's called again later.
+    if (!project.apolloProjectService.isInitialized) return emptyList()
+    return listOf(
+        GraphQLConfig(
+            project = project,
+            dir = projectDir,
+            file = null,
+            rawData = GraphQLRawConfig(
+                projects = project.service<GradleToolingModelService>().graphQLProjectFiles.map { graphQLProjectFiles ->
+                  graphQLProjectFiles.name to graphQLProjectFiles.toGraphQLRawProjectConfig()
+                }.toMap()
+            )
+        )
+    )
+  }
+
+  private fun GraphQLProjectFiles.toGraphQLRawProjectConfig() = GraphQLRawProjectConfig(
+      schema = schemaPaths.map { GraphQLRawSchemaPointer(it) },
+      include = operationPaths.map { "$it/*.graphql" },
+  )
+}
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLConfigService.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLConfigService.kt
new file mode 100644
index 0000000000..8fb934c1de
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLConfigService.kt
@@ -0,0 +1,25 @@
+package com.apollographql.ijplugin.graphql
+
+import com.apollographql.ijplugin.util.logd
+import com.intellij.lang.jsgraphql.ide.config.GraphQLConfigProvider
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+
+class GraphQLConfigService(
+    private val project: Project,
+) : Disposable {
+  init {
+    logd("project=${project.name}")
+    project.messageBus.connect(this).subscribe(GraphQLProjectFilesListener.TOPIC, object : GraphQLProjectFilesListener {
+      override fun projectFilesAvailable() {
+        logd("Calling scheduleConfigurationReload")
+        project.service<GraphQLConfigProvider>().scheduleConfigurationReload()
+      }
+    })
+  }
+
+  override fun dispose() {
+    logd("project=${project.name}")
+  }
+}
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLProjectFiles.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLProjectFiles.kt
new file mode 100644
index 0000000000..3c2e809da0
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/graphql/GraphQLProjectFiles.kt
@@ -0,0 +1,18 @@
+package com.apollographql.ijplugin.graphql
+
+import com.intellij.util.messages.Topic
+
+interface GraphQLProjectFilesListener {
+  companion object {
+    @Topic.ProjectLevel
+    val TOPIC: Topic<GraphQLProjectFilesListener> = Topic.create("GraphQLProjectFiles are available", GraphQLProjectFilesListener::class.java)
+  }
+
+  fun projectFilesAvailable()
+}
+
+data class GraphQLProjectFiles(
+    val name: String,
+    val schemaPaths: List<String>,
+    val operationPaths: List<String>,
+)
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectManagerListener.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectManagerListener.kt
index 805ec07974..de019ffa72 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectManagerListener.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectManagerListener.kt
@@ -2,6 +2,7 @@ package com.apollographql.ijplugin.project
 
 import com.apollographql.ijplugin.codegen.ApolloCodegenService
 import com.apollographql.ijplugin.gradle.GradleToolingModelService
+import com.apollographql.ijplugin.graphql.GraphQLConfigService
 import com.apollographql.ijplugin.settings.SettingsService
 import com.apollographql.ijplugin.util.logd
 import com.intellij.openapi.components.service
@@ -20,6 +21,9 @@ internal class ApolloProjectManagerListener : ProjectManagerListener {
       project.service<ApolloCodegenService>()
       project.service<GradleToolingModelService>()
       project.service<SettingsService>()
+      project.service<GraphQLConfigService>()
+
+      project.apolloProjectService.isInitialized = true
     }
   }
 }
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectService.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectService.kt
index fe027efd25..f073118105 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectService.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectService.kt
@@ -4,6 +4,7 @@ import com.intellij.openapi.components.service
 import com.intellij.openapi.project.Project
 
 interface ApolloProjectService {
+  var isInitialized: Boolean
   val isApolloAndroid2Project: Boolean
   val isApolloKotlin3Project: Boolean
 }
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectServiceImpl.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectServiceImpl.kt
index ea7f6cca83..a9d363029d 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectServiceImpl.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/project/ApolloProjectServiceImpl.kt
@@ -15,6 +15,7 @@ class ApolloProjectServiceImpl(
 
   override var isApolloAndroid2Project: Boolean = false
   override var isApolloKotlin3Project: Boolean = false
+  override var isInitialized: Boolean = false
 
   init {
     logd("project=${project.name}")
diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml
index 8b9a48450d..c635922f05 100644
--- a/intellij-plugin/src/main/resources/META-INF/plugin.xml
+++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml
@@ -54,6 +54,14 @@
     <!-- Tooling model -->
     <projectService serviceImplementation="com.apollographql.ijplugin.gradle.GradleToolingModelService" />
 
+    <!-- Listens to availability of Tooling model, and notifies the GraphQL plugin -->
+    <projectService serviceImplementation="com.apollographql.ijplugin.graphql.GraphQLConfigService" />
+
+  </extensions>
+
+  <!-- Contribute configuration to the GraphQL plugin -->
+  <extensions defaultExtensionNs="com.intellij.lang.jsgraphql">
+    <configContributor implementation="com.apollographql.ijplugin.graphql.ApolloGraphQLConfigContributor" />
   </extensions>
 
   <applicationListeners>
diff --git a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties
index 49ab01dab8..7489d47265 100644
--- a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties
+++ b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties
@@ -38,6 +38,7 @@ settings.codegen.automaticCodegenTriggering.comment=Enabling this setting will r
   This watches changes to your GraphQL files and re-generates models when they change.
 settings.codegen.title=Code Generation
 
+GradleToolingModelService.fetchToolingModels.progress=Loading Apollo Kotlin configuration
 
 CompatToOperationBasedCodegenMigrationProcessor.title=Migrate to OperationBased Codegen
 CompatToOperationBasedCodegenMigrationProcessor.noUsage=No Compat codegen usage found in the project
diff --git a/libraries/apollo-runtime/api/apollo-runtime.api b/libraries/apollo-runtime/api/apollo-runtime.api
index 881dda9ab4..79646785b7 100644
--- a/libraries/apollo-runtime/api/apollo-runtime.api
+++ b/libraries/apollo-runtime/api/apollo-runtime.api
@@ -109,6 +109,7 @@ public final class com/apollographql/apollo3/ApolloClient$Builder : com/apollogr
 	public final fun webSocketIdleTimeoutMillis (J)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun webSocketReopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun webSocketServerUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/ApolloClient$Builder;
+	public final fun webSocketServerUrl (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun wsProtocol (Lcom/apollographql/apollo3/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 }
 
@@ -425,7 +426,7 @@ public final class com/apollographql/apollo3/network/ws/WebSocketEngineKt {
 }
 
 public final class com/apollographql/apollo3/network/ws/WebSocketNetworkTransport : com/apollographql/apollo3/network/NetworkTransport {
-	public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/apollographql/apollo3/network/ws/WebSocketEngine;JLcom/apollographql/apollo3/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo3/network/ws/WebSocketEngine;JLcom/apollographql/apollo3/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public final fun closeConnection (Ljava/lang/Throwable;)V
 	public fun dispose ()V
 	public fun execute (Lcom/apollographql/apollo3/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow;
@@ -442,6 +443,7 @@ public final class com/apollographql/apollo3/network/ws/WebSocketNetworkTranspor
 	public final fun protocol (Lcom/apollographql/apollo3/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun reopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
+	public final fun serverUrl (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun webSocketEngine (Lcom/apollographql/apollo3/network/ws/WebSocketEngine;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 }
 
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
index cdf34ea006..87802d80fe 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
@@ -197,6 +197,7 @@ private constructor(
     private var httpExposeErrorBody: Boolean? = null
     private var webSocketEngine: WebSocketEngine? = null
     private var webSocketReopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)? = null
+    private var webSocketReopenServerUrl: (suspend () -> String)? = null
 
     init {
       failOnNativeIfLegacyMemoryManager()
@@ -316,6 +317,7 @@ private constructor(
 
     /**
      * The url of the GraphQL server used for WebSockets
+     * Use this function or webSocketServerUrl((suspend () -> String)) but not both.
      *
      * See also [subscriptionNetworkTransport] for more customization
      */
@@ -323,6 +325,20 @@ private constructor(
       this.webSocketServerUrl = webSocketServerUrl
     }
 
+    /**
+     * Configure dynamically the url of the GraphQL server used for WebSockets.
+     * Use this function or webSocketServerUrl(String) but not both.
+     *
+     * @param webSocketServerUrl a function returning the new server URL.
+     * This function will be called every time a WebSocket is opened. For example, you can use it to update your
+     * auth credentials in case of an unauthorized error.
+     *
+     * It is a suspending function, so it can be used to introduce delay before setting the new server URL.
+     */
+    fun webSocketServerUrl(webSocketServerUrl: (suspend () -> String)) = apply {
+      this.webSocketReopenServerUrl = webSocketServerUrl
+    }
+
     /**
      * The timeout after which an inactive WebSocket will be closed
      *
@@ -527,6 +543,9 @@ private constructor(
         check(webSocketReopenWhen == null) {
           "Apollo: 'webSocketReopenWhen' has no effect if 'subscriptionNetworkTransport' is set"
         }
+        check(webSocketReopenServerUrl == null) {
+          "Apollo: 'webSocketReopenServerUrl' has no effect if 'subscriptionNetworkTransport' is set"
+        }
         subscriptionNetworkTransport!!
       } else {
         val url = webSocketServerUrl ?: httpServerUrl
@@ -550,6 +569,9 @@ private constructor(
                 if (webSocketReopenWhen != null) {
                   reopenWhen(webSocketReopenWhen)
                 }
+                if (webSocketReopenServerUrl != null) {
+                  serverUrl(webSocketReopenServerUrl)
+                }
               }
               .build()
         }
@@ -599,6 +621,7 @@ private constructor(
       }
       subscriptionNetworkTransport?.let { builder.subscriptionNetworkTransport(it) }
       webSocketServerUrl?.let { builder.webSocketServerUrl(it) }
+      webSocketReopenServerUrl?.let { builder.webSocketServerUrl(it) }
       webSocketEngine?.let { builder.webSocketEngine(it) }
       webSocketReopenWhen?.let { builder.webSocketReopenWhen(it) }
       webSocketIdleTimeoutMillis?.let { builder.webSocketIdleTimeoutMillis(it) }
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
index 06cd29803d..6059b11636 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
@@ -57,12 +57,12 @@ import okio.use
  */
 class WebSocketNetworkTransport
 private constructor(
-    private val serverUrl: String,
+    private val serverUrl: (suspend () -> String),
     private val headers: List<HttpHeader>,
     private val webSocketEngine: WebSocketEngine = DefaultWebSocketEngine(),
     private val idleTimeoutMillis: Long = 60_000,
     private val protocolFactory: WsProtocol.Factory = SubscriptionWsProtocol.Factory(),
-    private val reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)?,
+    private val reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)?
 ) : NetworkTransport {
 
   /**
@@ -187,7 +187,7 @@ private constructor(
 
             val webSocketConnection = try {
               webSocketEngine.open(
-                  url = serverUrl,
+                  url = serverUrl(),
                   headers = if (headers.any { it.name == "Sec-WebSocket-Protocol" }) {
                     headers
                   } else {
@@ -347,7 +347,7 @@ private constructor(
   }
 
   class Builder {
-    private var serverUrl: String? = null
+    private var serverUrl: (suspend () -> String)? = null
     private var headers: MutableList<HttpHeader> = mutableListOf()
     private var webSocketEngine: WebSocketEngine? = null
     private var idleTimeoutMillis: Long? = null
@@ -355,6 +355,19 @@ private constructor(
     private var reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)? = null
 
     fun serverUrl(serverUrl: String) = apply {
+      this.serverUrl = { serverUrl }
+    }
+
+    /**
+     * Configure the server URL dynamically.
+     *
+     * @param serverUrl a function returning the new server URL.
+     * This function will be called every time a WebSocket is opened. For example, you can use it to update your
+     * auth credentials in case of an unauthorized error.
+     *
+     * It is a suspending function, so it can be used to introduce delay before setting the new serverUrl.
+     */
+    fun serverUrl(serverUrl: (suspend () -> String)?) = apply {
       this.serverUrl = serverUrl
     }
 
@@ -398,14 +411,13 @@ private constructor(
     }
 
     fun build(): WebSocketNetworkTransport {
-      @Suppress("DEPRECATION")
       return WebSocketNetworkTransport(
-          serverUrl ?: error("No serverUrl specified"),
-          headers,
-          webSocketEngine ?: DefaultWebSocketEngine(),
-          idleTimeoutMillis ?: 60_000,
-          protocolFactory ?: SubscriptionWsProtocol.Factory(),
-          reopenWhen
+          serverUrl = serverUrl ?: error("No serverUrl specified"),
+          headers = headers,
+          webSocketEngine = webSocketEngine ?: DefaultWebSocketEngine(),
+          idleTimeoutMillis = idleTimeoutMillis ?: 60_000,
+          protocolFactory = protocolFactory ?: SubscriptionWsProtocol.Factory(),
+          reopenWhen = reopenWhen
       )
     }
   }
diff --git a/renovate.json b/renovate.json
index d00cb399fb..7170df8898 100644
--- a/renovate.json
+++ b/renovate.json
@@ -3,7 +3,8 @@
   "ignorePaths": [],
   "includePaths": [
     "settings.gradle.kts",
-    "tests/settings.gradle.kts"
+    "tests/settings.gradle.kts",
+    "build-logic/settings.gradle.kts"
   ],
   "timeout": 600,
   "extends": ["config:base"]
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 059f8b0722..a307b87b37 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -20,7 +20,7 @@ pluginManagement {
 }
 
 plugins {
-  id("com.gradle.enterprise") version "3.13"
+  id("com.gradle.enterprise") version "3.13" // sync with libraries.toml
   id("com.gradle.common-custom-user-data-gradle-plugin") version "1.10"
   id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0"
 }


Test patch:
diff --git a/build-logic/src/main/kotlin/Testing.kt b/build-logic/src/main/kotlin/Testing.kt
index a6d5d51787..b105aa6939 100644
--- a/build-logic/src/main/kotlin/Testing.kt
+++ b/build-logic/src/main/kotlin/Testing.kt
@@ -1,3 +1,4 @@
+import com.gradle.enterprise.gradleplugin.testretry.retry
 import org.gradle.api.Project
 import org.gradle.api.tasks.testing.AbstractTestTask
 import org.gradle.api.tasks.testing.Test
@@ -9,6 +10,18 @@ fun Project.configureTesting() {
     systemProperty("updateTestFixtures", System.getProperty("updateTestFixtures"))
     systemProperty("testFilter", System.getProperty("testFilter"))
     systemProperty("codegenModels", System.getProperty("codegenModels"))
+
+  }
+  
+  pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
+    tasks.withType(Test::class.java) {
+      retry {
+        if (System.getenv().containsKey("CI")) {
+          maxRetries.set(3)
+          failOnPassedAfterRetry.set(true)
+        }
+      }
+    }
   }
 
   tasks.withType(AbstractTestTask::class.java) {
diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts
index 7549028f2b..8c22996d4e 100644
--- a/tests/settings.gradle.kts
+++ b/tests/settings.gradle.kts
@@ -25,7 +25,7 @@ pluginManagement {
 }
 
 plugins {
-  id("com.gradle.enterprise") version "3.13"
+  id("com.gradle.enterprise") version "3.13" // sync with libraries.toml
   id("com.gradle.common-custom-user-data-gradle-plugin") version "1.10"
   id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0"
 }