diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c6cab5ec..8832eb9a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed crash when using 3rd party loggers that don't implement `setLevel`. (#8631) +- Fixed "Slow operations are prohibited on EDT" by migrating `FlutterProjectOpenProcessor` to Kotlin and using `openProjectAsync`. (#8629) ## 88.1.0 diff --git a/build.gradle.kts b/build.gradle.kts index f17d9de79b..2c70a28158 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -141,6 +141,12 @@ sourceSets { "third_party/vmServiceDrivers" ) ) + kotlin.srcDirs( + listOf( + "src", + "third_party/vmServiceDrivers" + ) + ) // Add kotlin.srcDirs if we start using Kotlin in the main plugin. resources.srcDirs( listOf( diff --git a/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.java b/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.java deleted file mode 100644 index a6681b5a21..0000000000 --- a/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2018 The Chromium Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ -package io.flutter.editor; - -import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.projectImport.ProjectOpenProcessor; -import com.intellij.ui.EditorNotifications; -import io.flutter.FlutterUtils; -import io.flutter.project.FlutterProjectOpenProcessor; -import io.flutter.pub.PubRoot; -import io.flutter.utils.FlutterModuleUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class FlutterStudioProjectOpenProcessor extends FlutterProjectOpenProcessor { - @Override - public @NotNull String getName() { - return "Flutter Studio"; - } - - @Override - public boolean canOpenProject(@Nullable VirtualFile file) { - if (file == null) return false; - final PubRoot root = PubRoot.forDirectory(file); - return root != null && root.declaresFlutter(); - } - - @Nullable - @Override - public Project doOpenProject(@NotNull VirtualFile virtualFile, @Nullable Project projectToClose, boolean forceOpenInNewFrame) { - final ProjectOpenProcessor importProvider = getDelegateImportProvider(virtualFile); - if (importProvider == null) return null; - - importProvider.doOpenProject(virtualFile, projectToClose, forceOpenInNewFrame); - // A callback may have caused the project to be reloaded. Find the new Project object. - Project project = FlutterUtils.findProject(virtualFile.getPath()); - if (project == null || project.isDisposed()) { - return project; - } - for (Module module : FlutterModuleUtils.getModules(project)) { - if (FlutterModuleUtils.declaresFlutter(module) && !FlutterModuleUtils.isFlutterModule(module)) { - FlutterModuleUtils.setFlutterModuleType(module); - FlutterModuleUtils.enableDartSDK(module); - } - } - project.save(); - EditorNotifications.getInstance(project).updateAllNotifications(); - - //FlutterProjectCreator.disableUserConfig(project); - return project; - } -} diff --git a/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.kt b/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.kt new file mode 100644 index 0000000000..fae83178f5 --- /dev/null +++ b/src/io/flutter/editor/FlutterStudioProjectOpenProcessor.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter.editor + +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.projectImport.ProjectOpenProcessor +import com.intellij.ui.EditorNotifications +import io.flutter.FlutterUtils +import io.flutter.project.FlutterProjectOpenProcessor +import io.flutter.pub.PubRoot +import io.flutter.utils.FlutterModuleUtils + +/** + * Originally `FlutterStudioProjectOpenProcessor.java`. + * + * This processor is specific to Android Studio (or when the Flutter Studio plugin is active). + * It extends `FlutterProjectOpenProcessor` to provide specialized handling for Android Studio, + * particularly ensuring that the project is correctly re-detected if the opening process causes a reload. + * + * Converted to Kotlin to support `openProjectAsync`. + */ +class FlutterStudioProjectOpenProcessor : FlutterProjectOpenProcessor() { + override val name: String + get() = "Flutter Studio" + + override fun canOpenProject(file: VirtualFile): Boolean { + val root = PubRoot.forDirectory(file) + return root != null && root.declaresFlutter() + } + + /** + * Replaces the deprecated `doOpenProject`. + * + * Performs the same logic as the original Java implementation but using `suspend` and `writeAction`. + * + * Key differences from the base class: + * - Explicitly looks up the project again using `FlutterUtils.findProject` after opening, as the project instance might have changed + * (e.g. if the platform closed and reopened it during import). + * - Ensures Dart SDK is enabled and notifications are updated. + */ + override suspend fun openProjectAsync( + virtualFile: VirtualFile, + projectToClose: Project?, + forceOpenInNewFrame: Boolean, + ): Project? { + val importProvider = getDelegateImportProvider(virtualFile) ?: return null + val project = importProvider.openProjectAsync(virtualFile, projectToClose, forceOpenInNewFrame) + + // A callback may have caused the project to be reloaded. Find the new Project object. + val newProject = FlutterUtils.findProject(virtualFile.path) + if (newProject == null || newProject.isDisposed) { + return newProject + } + + writeAction { + for (module in FlutterModuleUtils.getModules(newProject)) { + if (FlutterModuleUtils.declaresFlutter(module) && !FlutterModuleUtils.isFlutterModule(module)) { + FlutterModuleUtils.setFlutterModuleType(module) + FlutterModuleUtils.enableDartSDK(module) + } + } + newProject.save() + EditorNotifications.getInstance(newProject).updateAllNotifications() + } + + return newProject + } + + override fun doOpenProject( + virtualFile: VirtualFile, + projectToClose: Project?, + forceOpenInNewFrame: Boolean, + ): Project? { + return null + } +} diff --git a/src/io/flutter/project/FlutterProjectOpenProcessor.java b/src/io/flutter/project/FlutterProjectOpenProcessor.java deleted file mode 100644 index 69427c9a98..0000000000 --- a/src/io/flutter/project/FlutterProjectOpenProcessor.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2017 The Chromium Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ -package io.flutter.project; - -import com.intellij.openapi.application.ApplicationInfo; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.projectImport.ProjectOpenProcessor; -import icons.FlutterIcons; -import io.flutter.FlutterBundle; -import io.flutter.FlutterUtils; -import io.flutter.ProjectOpenActivity; -import io.flutter.pub.PubRoot; -import io.flutter.utils.FlutterModuleUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.util.Objects; - -public class FlutterProjectOpenProcessor extends ProjectOpenProcessor { - - @Override - public @NotNull String getName() { - return FlutterBundle.message("flutter.module.name"); - } - - @Override - public Icon getIcon() { - return FlutterIcons.Flutter; - } - - @Override - public boolean canOpenProject(@Nullable VirtualFile file) { - if (file == null) return false; - final ApplicationInfo info = ApplicationInfo.getInstance(); - if (FlutterUtils.isAndroidStudio()) { - return false; - } - final PubRoot root = PubRoot.forDirectory(file); - return root != null && root.declaresFlutter(); - } - - /** - * Runs when a project is opened by selecting the project directly, possibly for import. - *
- * Doesn't run when a project is opened via recent projects menu (and so on). Actions that - * should run every time a project is opened should be in - * {@link ProjectOpenActivity} or {@link io.flutter.FlutterInitializer}. - */ - @Nullable - @Override - public Project doOpenProject(@NotNull VirtualFile file, @Nullable Project projectToClose, boolean forceOpenInNewFrame) { - // Delegate opening to the platform open processor. - final ProjectOpenProcessor importProvider = getDelegateImportProvider(file); - if (importProvider == null) return null; - - final Project project = importProvider.doOpenProject(file, projectToClose, forceOpenInNewFrame); - if (project == null || project.isDisposed()) return project; - - // Convert any modules that use Flutter but don't have IntelliJ Flutter metadata. - convertToFlutterProject(project); - - // Project gets reloaded; should this be: FlutterUtils.findProject(file.getPath()); - return project; - } - - @Nullable - protected ProjectOpenProcessor getDelegateImportProvider(@NotNull VirtualFile file) { - //noinspection DataFlowIssue - return EXTENSION_POINT_NAME.getExtensionList().stream().filter( - processor -> processor.canOpenProject(file) && !Objects.equals(processor.getName(), getName()) - ).findFirst().orElse(null); - } - - /** - * Sets up a project that doesn't have any Flutter modules. - *
- * (It probably wasn't created with "flutter create" and probably didn't have any IntelliJ configuration before.) - */ - private static void convertToFlutterProject(@NotNull Project project) { - for (Module module : FlutterModuleUtils.getModules(project)) { - if (FlutterModuleUtils.declaresFlutter(module) && !FlutterModuleUtils.isFlutterModule(module)) { - FlutterModuleUtils.setFlutterModuleAndReload(module, project); - } - } - } - - @Override - public boolean isStrongProjectInfoHolder() { - return true; - } -} diff --git a/src/io/flutter/project/FlutterProjectOpenProcessor.kt b/src/io/flutter/project/FlutterProjectOpenProcessor.kt new file mode 100644 index 0000000000..dab85dac64 --- /dev/null +++ b/src/io/flutter/project/FlutterProjectOpenProcessor.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter.project + +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.projectImport.ProjectOpenProcessor +import icons.FlutterIcons +import io.flutter.FlutterBundle +import io.flutter.FlutterUtils +import io.flutter.pub.PubRoot +import io.flutter.utils.FlutterModuleUtils +import java.util.Objects +import javax.swing.Icon + +/** + * Originally `FlutterProjectOpenProcessor.java`. + * + * This processor handles opening Flutter projects when they are selected directly (e.g. via "Open" in the IDE). + * It delegates the actual opening to the platform's default processor (e.g. Gradle or Maven processor if applicable, + * or the generic project opener) and then ensures that any modules in the project are correctly configured as Flutter modules. + * + * Converted to Kotlin to support `openProjectAsync` which is a suspend function. + */ +open class FlutterProjectOpenProcessor : ProjectOpenProcessor() { + override val name: String + get() = FlutterBundle.message("flutter.module.name") + + override fun getIcon(file: VirtualFile): Icon? { + return FlutterIcons.Flutter + } + + override fun canOpenProject(file: VirtualFile): Boolean { + val info = ApplicationInfo.getInstance() + if (FlutterUtils.isAndroidStudio()) { + return false + } + val root = PubRoot.forDirectory(file) + return root != null && root.declaresFlutter() + } + + /** + * Replaces the deprecated `doOpenProject`. + * + * This method is `suspend` and must be used instead of `doOpenProject` to avoid `IllegalStateException` in newer IDE versions. + * + * It performs the following steps: + * 1. Finds a delegate processor (e.g. Gradle) to open the project. + * 2. Opens the project asynchronously. + * 3. Once opened, checks if the project contains Flutter modules that are not yet configured as such (e.g. missing module type). + * 4. Configures these modules as Flutter modules within a write action. + */ + override suspend fun openProjectAsync( + virtualFile: VirtualFile, + projectToClose: Project?, + forceOpenInNewFrame: Boolean, + ): Project? { + // Delegate opening to the platform open processor. + val importProvider = getDelegateImportProvider(virtualFile) ?: return null + val project = importProvider.openProjectAsync(virtualFile, projectToClose, forceOpenInNewFrame) + if (project == null || project.isDisposed) return project + + // Convert any modules that use Flutter but don't have IntelliJ Flutter metadata. + writeAction { + convertToFlutterProject(project) + } + + return project + } + + /** + * Deprecated method, kept to satisfy the compiler/interface. + * + * We return `null` to indicate that this processor does not support the synchronous opening method + * and that `openProjectAsync` should be used instead. + */ + override fun doOpenProject( + virtualFile: VirtualFile, + projectToClose: Project?, + forceOpenInNewFrame: Boolean, + ): Project? { + return null + } + + protected open fun getDelegateImportProvider(file: VirtualFile): ProjectOpenProcessor? { + return EXTENSION_POINT_NAME.extensionList.stream().filter { processor: ProjectOpenProcessor -> + processor.canOpenProject(file) && !Objects.equals( + processor.name, + name + ) + }.findFirst().orElse(null) + } + + + companion object { + /** + * Sets up a project that doesn't have any Flutter modules. + * + * + * (It probably wasn't created with "flutter create" and probably didn't have any IntelliJ configuration before.) + */ + private fun convertToFlutterProject(project: Project) { + for (module in FlutterModuleUtils.getModules(project)) { + if (FlutterModuleUtils.declaresFlutter(module) && !FlutterModuleUtils.isFlutterModule(module)) { + FlutterModuleUtils.setFlutterModuleAndReload(module, project) + } + } + } + } +}