diff --git a/.aiconfig b/.aiconfig new file mode 100644 index 0000000000..308e96ff48 --- /dev/null +++ b/.aiconfig @@ -0,0 +1,21 @@ +# Gemini Code Assist Configuration +version: 1.0 +project: + context: + instruction_files: + - .gemini/styleguide.md + + generation_rules: + - "Strictly adhere to the IntelliJ Platform Threading Model: No I/O on the EDT." + - "All generated AnAction classes must be stateless." + - "Apply [MUST-FIX], [CONCERN], and [NIT] severity logic to any code suggestions that violate plugin SDK best practices or style guidelines." + - "Use io.flutter.logging.PluginLogger for all logging; avoid System.out." + - "Always include the standard Chromium Authors copyright header in new files." + - "Adhere to the Zero-Formatting Policy: Do not comment on indentation, spacing, or brace placement." + # Ensure local agents read styleguide files automatically. + - "At the start of the session, read and adhere to the guidelines in all files listed in project.context.instruction_files." + + languages: + - java + - kotlin + - dart \ No newline at end of file diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index caf2b51937..c97d94e9fb 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -13,6 +13,8 @@ enforce standard modern Java/Kotlin coding conventions, but strictly police the - `[NIT]`: Idiomatic improvements or minor naming suggestions. - **Focus:** Prioritize logic, performance on the UI thread, and architectural consistency. - **No Empty Praise:** Do not leave "Looks good" or "Nice change" comments. If there are no issues, leave no comments. +- **Copyright Headers:** Ensure all new files have a proper copyright header (e.g., `Copyright 2026 The Chromium Authors`). Flag any missing + headers as `[MUST-FIX]`. ## 2. IntelliJ Platform Best Practices @@ -30,8 +32,10 @@ enforce standard modern Java/Kotlin coding conventions, but strictly police the as recommended by the modern SDK. - **Backward Compatibility:** Avoid using `@ApiStatus.Internal` or `@ApiStatus.ScheduledForRemoval` APIs unless strictly necessary. - **Logging:** - - Reject any use of `System.out.println` or `System.err.println` for logging in `src/` code (integration tests may use them for milestone logging). - - Enforce the use of the IntelliJ SDK's built-in logger (`com.intellij.openapi.diagnostic.Logger`) or our own (`io.flutter.logging.PluginLogger`). + - Reject any use of `System.out.println` or `System.err.println` for logging in `src/` code (integration tests may use them for + milestone logging). + - Enforce the use of the IntelliJ SDK's built-in logger (`com.intellij.openapi.diagnostic.Logger`) or our own ( + `io.flutter.logging.PluginLogger`). - **Actions:** - Classes extending `AnAction` must be completely stateless. Flag any `AnAction` class that defines mutable instance variables (fields), as the platform instantiates a single instance of the action for the lifetime of the IDE. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 109ca2991d..ba26e31e05 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,3 @@ -# See Dependabot documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - package-ecosystem: "github-actions" @@ -14,3 +11,10 @@ updates: ignore: - dependency-name: "*" update-types: ["version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + labels: + - "autosubmit" \ No newline at end of file diff --git a/.github/workflows/presubmit.yaml b/.github/workflows/presubmit.yaml index 7ab6c5ec11..c10d8ebead 100644 --- a/.github/workflows/presubmit.yaml +++ b/.github/workflows/presubmit.yaml @@ -37,7 +37,7 @@ jobs: env: BOT: ${{ matrix.bot }} - name: upload build artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: build-${{ matrix.bot }} diff --git a/AUTHORS b/AUTHORS index 8b8de49417..5a33b3d9d7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,3 +26,5 @@ Mohamed El Sayed Edwin Ludik Japnit Singh Dmitry Kandalov +Kazuya Chikamatsu +Dustin Feucht diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f30b06d4d..ad0b4b8c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,30 @@ ### Added -### Changed +- Option to specify the pub root module for Flutter Widget Previewer. (#8888) -- Updated gradle plugin version to re-enable running `./gradlew verifyPlugin` locally. (#8847) +### Changed ### Removed +### Fixed +- Silent failure when opening Flutter projects without `.idea` directory in IntelliJ IDEA, by removing `FlutterProjectOpenProcessor` and + migrating configuration logic to `FlutterInitializer`. (#8903) +- IDE focus loss when running in full screen mode. (#8906) +- Excessive toolbar updates causing exceptions in Android Studio, by making device selector updates targeted instead of global. (#8891) + +## 91.0.0 + +### Changed + +- Gradle plugin version to re-enable running `./gradlew verifyPlugin` locally. (#8847) + ### Fixed -- Silent failure when opening Flutter projects without `.idea` directory in IntelliJ IDEA, by removing `FlutterProjectOpenProcessor` and migrating configuration logic to `FlutterInitializer`. (#8845) +- Gutter buttons not running tests with non-ASCII characters in their names. (#8838) +- Freeze from JX Browser close. (#8864) +- Crash in split debugger mode in IntelliJ 2025.3+. (#8878) +- Passing additional arguments from the Flutter test template. (#8836) ## 90.0.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..b832fac27e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,12 @@ +# Flutter IntelliJ Plugin — Claude Code Guide + +@.gemini/styleguide.md + +## Additional Rules + +- No I/O or heavy computation on the EDT (IntelliJ Threading Model). +- All `AnAction` subclasses must be stateless (no mutable instance fields). +- Use `io.flutter.logging.PluginLogger` (or IntelliJ's `Logger`) for all logging; never `System.out`. +- All new files must include the standard Chromium Authors copyright header. +- Zero-Formatting Policy: do not comment on indentation, spacing, or brace placement. +- Categorize code suggestions with `[MUST-FIX]`, `[CONCERN]`, or `[NIT]` severity prefixes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a82ede3ea..7ad5bc9ea8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,6 @@ name and contact info to the [AUTHORS](AUTHORS) file. ## Environment set-up 1. Install the latest [Java Development Kit](https://www.java.com/en/download/). - - The current Java Development Kit version is: **23**. - **[Googlers only]** Install Java from go/softwarecenter instead. 2. Set your `JAVA_HOME` directory in the configuration file for your shell environment. @@ -233,6 +232,23 @@ name and contact info to the [AUTHORS](AUTHORS) file. - Expand `Edit configuration templates...` and verify that Flutter is present. - Click [+] and verify that Flutter is present. +### Running against custom target IDEs + +To test or debug the plugin against a different IDE target (like IntelliJ IDEA Community or Ultimate) without changing the project's compilation target, you can use the custom `runTarget` Gradle task. + +Run it from the command line specifying the target IDE and version: +```bash +./gradlew runTarget -Pide=IntelliJ -PideV=2025.1 +``` + +* **`-Pide`**: Valid values are `AndroidStudio` (default), `IntelliJ` (Community), and `Ultimate`. +* **`-PideV`**: Any valid version string or build number for the selected IDE. + +To see a full list of available options and usage examples, run the task without any parameters: +```bash +./gradlew runTarget +``` + ## Provision Tool This is not currently required. However, for debugging unit tests, it may be handy; please ignore for now. diff --git a/build.gradle.kts b/build.gradle.kts index 992f124b9c..a851bf9b48 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,10 +40,10 @@ plugins { // https://plugins.gradle.org/plugin/org.jetbrains.intellij.platform // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm id("java") // Java support - id("org.jetbrains.intellij.platform") version "2.12.0" // IntelliJ Platform Gradle Plugin - id("org.jetbrains.kotlin.jvm") version "2.2.0" // Kotlin support - id("org.jetbrains.changelog") version "2.2.0" // Gradle Changelog Plugin - id("org.jetbrains.kotlinx.kover") version "0.9.4" + alias(libs.plugins.intellij.platform) // IntelliJ Platform Gradle Plugin + alias(libs.plugins.kotlin.jvm) // Kotlin support + alias(libs.plugins.changelog) // Gradle Changelog Plugin + alias(libs.plugins.kover) idea // IntelliJ IDEA support } @@ -239,13 +239,13 @@ dependencies { pluginVerifier() } - compileOnly("org.jetbrains:annotations:24.0.0") - testImplementation("org.jetbrains:annotations:24.0.0") - compileOnly("com.google.guava:guava:32.0.1-android") - compileOnly("com.google.code.gson:gson:2.10.1") - testImplementation("com.google.guava:guava:32.0.1-jre") - testImplementation("com.google.code.gson:gson:2.10.1") - testImplementation("junit:junit:4.13.2") + compileOnly(libs.jetbrains.annotations) + testImplementation(libs.jetbrains.annotations) + compileOnly(libs.guava.android) + compileOnly(libs.gson) + testImplementation(libs.guava.jre) + testImplementation(libs.gson) + testImplementation(libs.junit) implementation( fileTree( mapOf( @@ -256,12 +256,12 @@ dependencies { ) // UI Test dependencies - integrationImplementation("org.kodein.di:kodein-di-jvm:7.26.1") - integrationImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + integrationImplementation(libs.kodein.di) + integrationImplementation(libs.kotlinx.coroutines) // JUnit 5 is required for UI tests - integrationImplementation("org.junit.jupiter:junit-jupiter:5.11.4") - integrationRuntimeOnly("org.junit.platform:junit-platform-launcher") + integrationImplementation(libs.junit.jupiter) + integrationRuntimeOnly(libs.junit.platform.launcher) } intellijPlatform { @@ -389,12 +389,20 @@ tasks { // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#how-to-check-the-latest-available-eap-release tasks { printProductsReleases { - channels = listOf(ProductRelease.Channel.EAP) - types = listOf(IntelliJPlatformType.IntellijIdeaCommunity) + channels = listOf(ProductRelease.Channel.RELEASE, ProductRelease.Channel.EAP) + types = listOf(IntelliJPlatformType.IntellijIdeaCommunity, IntelliJPlatformType.IntellijIdeaUltimate, IntelliJPlatformType.AndroidStudio) untilBuild = provider { null } doLast { productsReleases.get().max() + println() + println("Mapping printProductsReleases output to ideV:") + println(" - The prefix (e.g., IU-, IC-, AI-) maps to -Pide (Ultimate, IntelliJ, Android Studio).") + println(" - The number part (e.g., 261.23567.71) maps to -PideV.") + println(" - Example: IU-261.23567.71 -> -Pide=Ultimate -PideV=261.23567.71") + println(" - Example: AI-2025.3.3.6 -> -Pide=AndroidStudio -PideV=2025.3.3.6") + + println() } } prepareJarSearchableOptions { @@ -415,6 +423,53 @@ tasks { } } +intellijPlatformTesting { + runIde { + register("runTarget") { + val target = project.findProperty("ide") as? String + val version = project.findProperty("ideV") as? String + + val actualTarget = target ?: "AndroidStudio" + type = when (actualTarget) { + "IntelliJ" -> IntelliJPlatformType.IntellijIdeaCommunity + "Ultimate" -> IntelliJPlatformType.IntellijIdeaUltimate + else -> IntelliJPlatformType.AndroidStudio + } + this.version = version ?: ideaVersion + } + } +} + +tasks.named("runTarget") { + val target = project.findProperty("ide") as? String + val version = project.findProperty("ideV") as? String + + doFirst { + if (target == null && version == null) { + println("============================================================") + println("runTarget - Available Options") + println("============================================================") + println("Valid values for -Pide:") + println(" - AndroidStudio (default)") + println(" - IntelliJ (IntelliJ IDEA Community)") + println(" - Ultimate (IntelliJ IDEA Ultimate)") + println() + println("Valid values for -PideV:") + println(" - Any valid version string for the selected IDE.") + println(" - Examples for IntelliJ/Ultimate: 2024.1, 2024.2, 2024.3, 2025.1") + println(" - Run './gradlew printProductsReleases' to see the full list.") + println() + println("Examples:") + println(" ./gradlew runTarget -Pide=IntelliJ -PideV=2025.1") + println(" ./gradlew runTarget -Pide=Ultimate -PideV=2025.1") + println("============================================================") + println("Stopping execution. Please run with parameters to launch a specific IDE.") + + throw org.gradle.api.tasks.StopExecutionException() + } + } +} + // A task to print the classpath used for compiling an IntelliJ plugin // Run with `./gradlew printCompileClasspath --no-configuration-cache ` tasks.register("printCompileClasspath") { diff --git a/gradle.properties b/gradle.properties index 16dbccfb1f..9186e9e84b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ # ideaVersion=2025.2.3.9 -dartPluginVersion= 503.0.0 +dartPluginVersion= 504.0.0 # Also update the versions for verify checks in tool/github.sh. sinceBuild=251 untilBuild=261.* diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..b866f33984 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,35 @@ +# Copyright 2026 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. + +[versions] +jetbrains-annotations = "24.0.0" +guava-android = "33.6.0-android" +guava-jre = "33.6.0-android" +gson = "2.10.1" +junit = "4.13.2" +kodein-di = "7.26.1" +kotlinx-coroutines = "1.9.0" +junit-jupiter = "6.0.3" +junit-platform = "6.0.3" +intellij-platform-plugin = "2.12.0" +kotlin = "2.2.0" +changelog = "2.5.0" +kover = "0.9.4" + +[libraries] +jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" } +guava-android = { group = "com.google.guava", name = "guava", version.ref = "guava-android" } +guava-jre = { group = "com.google.guava", name = "guava", version.ref = "guava-jre" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +kodein-di = { group = "org.kodein.di", name = "kodein-di-jvm", version.ref = "kodein-di" } +kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" } +junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junit-platform" } + +[plugins] +intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-platform-plugin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c74..d997cfc60f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 19a6bdeb84..c61a118f7d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf93008b7..739907dfd1 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21834..e509b2dd8f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/io/flutter/FlutterBundle.properties b/src/io/flutter/FlutterBundle.properties index d58de0f0b5..4d468d93f8 100644 --- a/src/io/flutter/FlutterBundle.properties +++ b/src/io/flutter/FlutterBundle.properties @@ -193,4 +193,9 @@ widget.preview.starting=Starting Flutter Widget Preview web server... widget.preview.loading=Loading Flutter Widget Preview at {0}... widget.preview.error=Error: {0} widget.preview.sdk.too.old=Flutter SDK version is too old for Flutter Widget Preview. Please update your SDK. +widget.preview.pubroot.not.found=Pub root could not be found to start Flutter Widget Preview. +widget.preview.retry=Reload and try again +widget.preview.choose_pubroot=Choose module and try again +widget.preview.choose_pubroot.title=Select Flutter Project +widget.preview.choose_pubroot.description=Select the Flutter project to use for Flutter Widget Preview. flutter.sdk.not.found=Flutter SDK not found. \ No newline at end of file diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index 7bc1e41078..d9d1ab6681 100644 --- a/src/io/flutter/FlutterInitializer.java +++ b/src/io/flutter/FlutterInitializer.java @@ -97,18 +97,6 @@ public void executeProjectStartup(@NotNull Project project) { // Start a DevTools server DevToolsService.getInstance(project); - // Ensure Flutter project configuration is applied for projects that may have been - // opened without a .idea directory. Previously this was handled by FlutterProjectOpenProcessor, - // but that processor silently failed when no delegate processor could open the project. - // Instead, we let the platform open the project normally and apply our configuration here. - // See https://github.com/flutter/flutter-intellij/issues/8661 (Android Studio equivalent) - for (Module module : FlutterModuleUtils.getModules(project)) { - if (FlutterModuleUtils.declaresFlutter(module) && !FlutterModuleUtils.isFlutterModule(module)) { - log().info("Fixing Flutter module configuration for " + module.getName()); - FlutterModuleUtils.setFlutterModuleAndReload(module, project); - } - } - // If the project declares a Flutter dependency, do some extra initialization. boolean hasFlutterModule = false; @@ -125,8 +113,19 @@ public void executeProjectStartup(@NotNull Project project) { } log().info("Flutter module has been found for project: " + project.getName()); - // Ensure SDKs are configured; needed for clean module import. - FlutterModuleUtils.enableDartSDK(module); + + // Ensure Flutter project configuration is applied for projects that may have been + // opened without a .idea directory. Previously this was handled by FlutterProjectOpenProcessor, + // but that processor silently failed when no delegate processor could open the project. + // Instead, we let the platform open the project normally and apply our configuration here. + // See https://github.com/flutter/flutter-intellij/issues/8661 (Android Studio equivalent) + if (!FlutterModuleUtils.isFlutterModule(module)) { + log().info("Fixing Flutter module configuration for " + module.getName()); + FlutterModuleUtils.setFlutterModuleWithoutReload(module, project); + } else { + // Ensure SDKs are configured; needed for clean module import. + FlutterModuleUtils.enableDartSDK(module); + } for (PubRoot root : PubRoots.forModule(module)) { // Set Android SDK. diff --git a/src/io/flutter/actions/DeviceSelectorAction.java b/src/io/flutter/actions/DeviceSelectorAction.java index 812eb6b31e..c166b116a9 100644 --- a/src/io/flutter/actions/DeviceSelectorAction.java +++ b/src/io/flutter/actions/DeviceSelectorAction.java @@ -6,7 +6,6 @@ package io.flutter.actions; import com.intellij.icons.AllIcons; -import com.intellij.ide.ActivityTracker; import com.intellij.ide.DataManager; import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; @@ -46,6 +45,7 @@ import io.flutter.run.FlutterDevice; import io.flutter.run.daemon.DeviceService; import io.flutter.sdk.AndroidEmulatorManager; +import io.flutter.utils.AsyncUtils; import io.flutter.utils.FlutterModuleUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -381,14 +381,19 @@ public void projectClosing(@NotNull Project project) { update(project, presentation); } + updateComponent(project, presentation); + } + + private void updateComponent(@NotNull Project project, @NotNull Presentation presentation) { final DeviceService deviceService = DeviceService.getInstance(project); final FlutterDevice selectedDevice = deviceService.getSelectedDevice(); final Collection devices = deviceService.getConnectedDevices(); final String text; - Icon icon = DEFAULT_DEVICE_ICON; + Icon icon; if (devices.isEmpty()) { + icon = DEFAULT_DEVICE_ICON; final boolean isLoading = deviceService.getStatus() == DeviceService.State.LOADING; if (isLoading) { text = FlutterBundle.message("devicelist.loading"); @@ -398,6 +403,7 @@ public void projectClosing(@NotNull Project project) { } } else if (selectedDevice == null) { + icon = DEFAULT_DEVICE_ICON; text = FlutterBundle.message("devicelist.noDeviceSelected"); } else { @@ -412,25 +418,27 @@ else if (selectedDevice == null) { // Update the custom component if it exists final JButton customComponent = presentation.getClientProperty(CUSTOM_COMPONENT_KEY); if (customComponent != null) { - final @Nullable JBLabel iconLabel = (JBLabel)customComponent.getClientProperty(ICON_LABEL_KEY); - final @Nullable JBLabel textLabel = (JBLabel)customComponent.getClientProperty(TEXT_LABEL_KEY); + AsyncUtils.invokeLater(() -> { + final @Nullable JBLabel iconLabel = (JBLabel)customComponent.getClientProperty(ICON_LABEL_KEY); + final @Nullable JBLabel textLabel = (JBLabel)customComponent.getClientProperty(TEXT_LABEL_KEY); - if (iconLabel != null) { - iconLabel.setIcon(icon); - } - if (textLabel != null) { - textLabel.setText(text); - // Update the foreground color to adapt to theme changes. - textLabel.setForeground(getToolbarForegroundColor()); - customComponent.invalidate(); - Container parent = customComponent.getParent(); - while (parent != null) { - parent.invalidate(); - parent = parent.getParent(); + if (iconLabel != null) { + iconLabel.setIcon(icon); } - customComponent.revalidate(); - customComponent.repaint(); - } + if (textLabel != null) { + textLabel.setText(text); + // Update the foreground color to adapt to theme changes. + textLabel.setForeground(getToolbarForegroundColor()); + customComponent.invalidate(); + Container parent = customComponent.getParent(); + while (parent != null) { + parent.invalidate(); + parent = parent.getParent(); + } + customComponent.revalidate(); + customComponent.repaint(); + } + }); } } @@ -447,6 +455,7 @@ private void update(@NotNull Project project, @NotNull Presentation presentation } updateActions(project, presentation); updateVisibility(project, presentation); + updateComponent(project, presentation); } private static void updateVisibility(final Project project, final @NotNull Presentation presentation) { @@ -531,11 +540,6 @@ private void updateActions(@NotNull Project project, @NotNull Presentation prese // Atomically replace the action list LOG.debug("[" + projectName + "] Replacing device selector actions"); this.actions = newActions; - - var tracker = ActivityTracker.getInstance(); - if (tracker != null) { - tracker.inc(); - } } private static class SelectDeviceAction extends AnAction { diff --git a/src/io/flutter/jxbrowser/EmbeddedBrowserEngine.java b/src/io/flutter/jxbrowser/EmbeddedBrowserEngine.java index 6198beb332..31277e8b71 100644 --- a/src/io/flutter/jxbrowser/EmbeddedBrowserEngine.java +++ b/src/io/flutter/jxbrowser/EmbeddedBrowserEngine.java @@ -5,10 +5,11 @@ */ package io.flutter.jxbrowser; -import com.intellij.openapi.application.ApplicationListener; +import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.SystemInfo; +import io.flutter.utils.OpenApiUtils; import com.teamdev.jxbrowser.engine.Engine; import com.teamdev.jxbrowser.engine.EngineOptions; import com.teamdev.jxbrowser.engine.PasswordStore; @@ -22,7 +23,7 @@ import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED; import static com.teamdev.jxbrowser.engine.RenderingMode.OFF_SCREEN; -public class EmbeddedBrowserEngine { +public class EmbeddedBrowserEngine implements Disposable { private static final @NotNull Logger LOG = PluginLogger.createLogger(EmbeddedBrowserEngine.class); private final Engine engine; @@ -60,22 +61,22 @@ public EmbeddedBrowserEngine() { } engine = temp; - ApplicationManager.getApplication().addApplicationListener(new ApplicationListener() { - @Override - public boolean canExitApplication() { - try { - if (engine != null && !engine.isClosed()) { - engine.close(); - } + } + + @Override + public void dispose() { + OpenApiUtils.safeExecuteOnPooledThread(() -> { + try { + if (engine != null && !engine.isClosed()) { + engine.close(); } - catch (Exception ex) { - if (FlutterSettings.getInstance().isFilePathLoggingEnabled()) { - LOG.info(ex); - } else { - LOG.info("Exception when closing JX Browser engine: " + ex.getMessage()); - } + } + catch (Exception ex) { + if (FlutterSettings.getInstance().isFilePathLoggingEnabled()) { + LOG.info(ex); + } else { + LOG.info("Exception when closing JX Browser engine: " + ex.getMessage()); } - return true; } }); } diff --git a/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java b/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java index 76cce7bca6..e1d915f206 100644 --- a/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java +++ b/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java @@ -28,6 +28,7 @@ import io.flutter.settings.FlutterSettings; import io.flutter.utils.AsyncUtils; import io.flutter.utils.JxBrowserUtils; +import io.flutter.utils.OpenApiUtils; import io.flutter.utils.ZoomLevelSelector; import io.flutter.view.EmbeddedBrowser; import io.flutter.view.EmbeddedTab; @@ -98,7 +99,18 @@ public void loadUrl(String url) { @Override public void close() { - this.browser.close(); + OpenApiUtils.safeExecuteOnPooledThread(() -> { + try { + this.browser.close(); + } + catch (Exception ex) { + if (FlutterSettings.getInstance().isFilePathLoggingEnabled()) { + LOG.info(ex); + } else { + LOG.info("Exception when closing JX Browser instance: " + ex.getMessage()); + } + } + }); } @Override diff --git a/src/io/flutter/run/AttachState.java b/src/io/flutter/run/AttachState.java index fe67d8c2ae..f655fe226e 100644 --- a/src/io/flutter/run/AttachState.java +++ b/src/io/flutter/run/AttachState.java @@ -47,6 +47,6 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws // Cache for use in console configuration, and for updating registered extensionRPCs. FlutterApp.addToEnvironment(env, app); final ExecutionResult result = setUpConsoleAndActions(app); - return createDebugSession(env, app, result).getRunContentDescriptor(); + return createDebugSession(env, app, result); } } diff --git a/src/io/flutter/run/FlutterDebugSessionUtils.java b/src/io/flutter/run/FlutterDebugSessionUtils.java new file mode 100644 index 0000000000..a41a1971fe --- /dev/null +++ b/src/io/flutter/run/FlutterDebugSessionUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 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.run; + +import com.intellij.execution.ExecutionException; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.xdebugger.XDebugProcessStarter; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebuggerManager; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// TODO(helin24): This class is using reflection to find experimental APIs that are only present in platform versions 2025.3+. We should be +// able to use the APIs directly once we are only supporting versions past 2025.3. +// See https://github.com/flutter/flutter-intellij/issues/8879. +public class FlutterDebugSessionUtils { + + private static final Method newSessionBuilderMethod; + private static final Method environmentMethod; + private static final Method startSessionMethod; + + static { + Method nsb = null; + Method env = null; + Method ss = null; + try { + nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class); + Class builderClass = nsb.getReturnType(); + env = builderClass.getMethod("environment", ExecutionEnvironment.class); + ss = builderClass.getMethod("startSession"); + } catch (NoSuchMethodException e) { + // Fallback for older platforms + } + newSessionBuilderMethod = nsb; + environmentMethod = env; + startSessionMethod = ss; + } + + public static @NotNull RunContentDescriptor startSessionAndGetDescriptor( + @NotNull XDebuggerManager manager, + @NotNull ExecutionEnvironment env, + @NotNull XDebugProcessStarter starter, + boolean muteBreakpoints) throws ExecutionException { + try { + if (newSessionBuilderMethod == null) { + throw new NoSuchMethodException("newSessionBuilder is not available"); + } + Object builder = newSessionBuilderMethod.invoke(manager, starter); + builder = environmentMethod.invoke(builder, env); + Object sessionResult = startSessionMethod.invoke(builder); + + if (muteBreakpoints) { + Method getSessionMethod = sessionResult.getClass().getMethod("getSession"); + XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult); + session.setBreakpointMuted(true); + } + + Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor"); + return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult); + + } catch (NoSuchMethodException e) { + // Fallback to old API for 2025.1 and older + XDebugSession session = manager.startSession(env, starter); + if (muteBreakpoints) { + session.setBreakpointMuted(true); + } + return session.getRunContentDescriptor(); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof ExecutionException) { + throw (ExecutionException) cause; + } + throw new ExecutionException("Failed to start debug session via reflection", cause != null ? cause : e); + } catch (Exception e) { + throw new ExecutionException("Failed with unexpected reflection error", e); + } + } +} diff --git a/src/io/flutter/run/LaunchState.java b/src/io/flutter/run/LaunchState.java index 90a720a0bb..18c8a6581c 100644 --- a/src/io/flutter/run/LaunchState.java +++ b/src/io/flutter/run/LaunchState.java @@ -160,7 +160,7 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws final RunContentDescriptor descriptor; if (launchMode.supportsDebugConnection()) { ToolWindowBadgeUpdater.updateBadgedIcon(app, project); - descriptor = createDebugSession(env, app, result).getRunContentDescriptor(); + descriptor = createDebugSession(env, app, result); } else { descriptor = new RunContentBuilder(result, env).showRunContent(env.getContentToReuse()); @@ -224,28 +224,22 @@ protected void showNoDeviceConnectedMessage(Project project) { } @NotNull - protected XDebugSession createDebugSession(@NotNull final ExecutionEnvironment env, - @NotNull final FlutterApp app, - @NotNull final ExecutionResult executionResult) + protected RunContentDescriptor createDebugSession(@NotNull final ExecutionEnvironment env, + @NotNull final FlutterApp app, + @NotNull final ExecutionResult executionResult) throws ExecutionException { final DartUrlResolver resolver = DartUrlResolver.getInstance(env.getProject(), sourceLocation); final FlutterPositionMapper mapper = createPositionMapper(env, app, resolver); final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject()); - final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() { + return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() { @Override @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { return new FlutterDebugProcess(app, env, session, executionResult, resolver, mapper); } - }); - - if (app.getMode() != RunMode.DEBUG) { - session.setBreakpointMuted(true); - } - - return session; + }, app.getMode() != RunMode.DEBUG); } @NotNull diff --git a/src/io/flutter/run/bazelTest/BazelTestRunner.java b/src/io/flutter/run/bazelTest/BazelTestRunner.java index 7ffa75c510..94bdad13ab 100644 --- a/src/io/flutter/run/bazelTest/BazelTestRunner.java +++ b/src/io/flutter/run/bazelTest/BazelTestRunner.java @@ -45,6 +45,7 @@ import io.flutter.bazel.WorkspaceCache; import io.flutter.logging.PluginLogger; import io.flutter.run.FlutterPositionMapper; +import io.flutter.run.FlutterDebugSessionUtils; import io.flutter.run.common.CommonTestConfigUtils; import io.flutter.run.test.FlutterTestRunner; import io.flutter.settings.FlutterSettings; @@ -100,15 +101,13 @@ protected RunContentDescriptor runInDebugger(@NotNull BazelTestLaunchState launc // Create the debug session. final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject()); - final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() { + return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() { @Override @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { return new BazelTestDebugProcess(env, session, executionResult, resolver, connector, mapper); } - }); - - return session.getRunContentDescriptor(); + }, false); } /** diff --git a/src/io/flutter/run/common/CommonTestConfigUtils.java b/src/io/flutter/run/common/CommonTestConfigUtils.java index ba5f72d821..7e54278331 100644 --- a/src/io/flutter/run/common/CommonTestConfigUtils.java +++ b/src/io/flutter/run/common/CommonTestConfigUtils.java @@ -130,13 +130,16 @@ public String findTestName(@Nullable PsiElement elt) { final DartCallExpression call = findEnclosingTestCall(elt, getTestsFromOutline(elt.getContainingFile())); if (call == null) return null; + return extractTestName(call); + } + + @VisibleForTesting + @Nullable + public String extractTestName(@NotNull DartCallExpression call) { final DartStringLiteralExpression lit = DartSyntax.getArgument(call, 0, DartStringLiteralExpression.class); if (lit == null) return null; - final String name = DartSyntax.unquote(lit); - if (name == null) return null; - - return StringUtil.escapeProperty(name, false); + return DartSyntax.unquote(lit); } /** diff --git a/src/io/flutter/run/daemon/DeviceService.java b/src/io/flutter/run/daemon/DeviceService.java index 72d65e90f9..c2a61c9edd 100644 --- a/src/io/flutter/run/daemon/DeviceService.java +++ b/src/io/flutter/run/daemon/DeviceService.java @@ -191,7 +191,6 @@ private void refreshDeviceDaemon() { if (project.isDisposed()) return; deviceDaemon.refresh(this::chooseNextDaemon); refreshInProgress = false; - ActivityTracker.getInstance().inc(); }); } diff --git a/src/io/flutter/run/test/FlutterTestRunner.java b/src/io/flutter/run/test/FlutterTestRunner.java index 55c8b1ee77..077b771afc 100644 --- a/src/io/flutter/run/test/FlutterTestRunner.java +++ b/src/io/flutter/run/test/FlutterTestRunner.java @@ -33,6 +33,7 @@ import com.intellij.xdebugger.XDebuggerManager; import com.jetbrains.lang.dart.util.DartUrlResolver; import io.flutter.FlutterUtils; +import io.flutter.run.FlutterDebugSessionUtils; import io.flutter.ObservatoryConnector; import io.flutter.logging.PluginLogger; import io.flutter.run.FlutterPositionMapper; @@ -210,15 +211,13 @@ protected RunContentDescriptor runInDebugger(@NotNull TestLaunchState launcher, // Create the debug session. final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject()); - final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() { + return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() { @Override @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { return new TestDebugProcess(env, session, executionResult, resolver, connector, mapper); } - }); - - return session.getRunContentDescriptor(); + }, false); } /** diff --git a/src/io/flutter/sdk/FlutterSdkVersion.java b/src/io/flutter/sdk/FlutterSdkVersion.java index 975337cee8..5614ad2024 100644 --- a/src/io/flutter/sdk/FlutterSdkVersion.java +++ b/src/io/flutter/sdk/FlutterSdkVersion.java @@ -26,11 +26,11 @@ public final class FlutterSdkVersion implements Comparable { * Note, this is for the Flutter SDK version, not the Dart SDK version, this mapping can be found: * Flutter SDK Release Archive list. *

- * This version was updated last on December 5, 2025. + * This version was updated last on March 26, 2026. */ @VisibleForTesting @NotNull - public static final FlutterSdkVersion MIN_SDK_SUPPORTED = new FlutterSdkVersion("3.16"); + public static final FlutterSdkVersion MIN_SDK_SUPPORTED = new FlutterSdkVersion("3.19.4"); /** * The minimum version of the Flutter SDK that will be supported for 3 more months. A version less than this is either not supported or @@ -39,11 +39,11 @@ public final class FlutterSdkVersion implements Comparable { * Note, this is for the Flutter SDK version, not the Dart SDK version, this mapping can be found: * Flutter SDK Release Archive list. *

- * This version was updated last on December 5, 2025. + * This version was updated last on March 26, 2026. */ @VisibleForTesting @NotNull - public static final FlutterSdkVersion MIN_SDK_WITHOUT_SUNSET_WARNING = new FlutterSdkVersion("3.19.4"); + public static final FlutterSdkVersion MIN_SDK_WITHOUT_SUNSET_WARNING = new FlutterSdkVersion("3.22.2"); @NotNull private static final FlutterSdkVersion MIN_SUPPORTS_TOOL_EVENT_STREAM = new FlutterSdkVersion("3.7.1"); diff --git a/src/io/flutter/utils/FlutterModuleUtils.java b/src/io/flutter/utils/FlutterModuleUtils.java index 3fc281e013..edc36b6928 100644 --- a/src/io/flutter/utils/FlutterModuleUtils.java +++ b/src/io/flutter/utils/FlutterModuleUtils.java @@ -373,7 +373,7 @@ public static void setFlutterModuleType(@NotNull Module module) { module.setModuleType(getModuleTypeIDForFlutter()); } - public static void setFlutterModuleAndReload(@NotNull Module module, @NotNull Project project) { + public static void setFlutterModuleWithoutReload(@NotNull Module module, @NotNull Project project) { if (project.isDisposed()) return; ApplicationManager.getApplication().invokeLater(() -> { ApplicationManager.getApplication().runWriteAction(() -> setFlutterModuleType(module)); @@ -381,7 +381,6 @@ public static void setFlutterModuleAndReload(@NotNull Module module, @NotNull Pr project.save(); EditorNotifications.getInstance(project).updateAllNotifications(); - ProjectManager.getInstance().reloadProject(project); }); } diff --git a/src/io/flutter/view/EmbeddedBrowser.java b/src/io/flutter/view/EmbeddedBrowser.java index 0bcb847b3a..a5326cc3e9 100644 --- a/src/io/flutter/view/EmbeddedBrowser.java +++ b/src/io/flutter/view/EmbeddedBrowser.java @@ -69,14 +69,15 @@ public void projectClosing(@NotNull Project project) { final Map tabs = windows.get(window); for (final String tabName : tabs.keySet()) { final BrowserTab tab = tabs.get(tabName); - if (tab.embeddedTab != null) { - try { - tab.embeddedTab.close(); + final EmbeddedTab embeddedTab = tab.embeddedTab; + if (embeddedTab != null) { + try { + embeddedTab.close(); + } + catch (Exception ex) { + logger().info(ex); + } } - catch (Exception ex) { - logger().info(ex); - } - } } tabs.clear(); } diff --git a/src/io/flutter/vmService/DartVmServiceDebugProcess.java b/src/io/flutter/vmService/DartVmServiceDebugProcess.java index 5179a63db8..3404996c13 100644 --- a/src/io/flutter/vmService/DartVmServiceDebugProcess.java +++ b/src/io/flutter/vmService/DartVmServiceDebugProcess.java @@ -98,23 +98,6 @@ public DartVmServiceDebugProcess(@NotNull final ExecutionEnvironment executionEn session.setPauseActionSupported(true); - session.addSessionListener(new XDebugSessionListener() { - @Override - public void sessionPaused() { - // This can be removed if XFramesView starts popping the project window to the top of the z-axis stack. - final Project project = getSession().getProject(); - focusProject(project); - stackFrameChanged(); - } - - @Override - public void stackFrameChanged() { - final XStackFrame stackFrame = getSession().getCurrentStackFrame(); - myLatestCurrentIsolateId = - stackFrame instanceof DartVmServiceStackFrame ? ((DartVmServiceStackFrame)stackFrame).getIsolateId() : null; - } - }); - this.executionEnvironment = executionEnvironment; this.mapper = mapper; myConnector = connector; @@ -594,68 +577,6 @@ private static boolean isDartPatchUri(@NotNull final String uri) { return uri.startsWith("dart:_") || uri.startsWith("dart:") && uri.contains("-patch/"); } - private static void focusProject(@NotNull Project project) { - final WindowManager windowManager = WindowManager.getInstance(); - if (windowManager == null) { - return; - } - - final JFrame projectFrame = windowManager.getFrame(project); - if (projectFrame == null) { - return; - } - - final int frameState = projectFrame.getExtendedState(); - - if (BitUtil.isSet(frameState, java.awt.Frame.ICONIFIED)) { - // restore the frame if it is minimized - projectFrame.setExtendedState(frameState ^ java.awt.Frame.ICONIFIED); - projectFrame.toFront(); - } - else { - final JFrame anchor = new JFrame(); - anchor.setType(Window.Type.UTILITY); - anchor.setUndecorated(true); - anchor.setSize(0, 0); - anchor.addWindowListener(new WindowListener() { - @Override - public void windowOpened(WindowEvent e) { - } - - @Override - public void windowClosing(WindowEvent e) { - } - - @Override - public void windowClosed(WindowEvent e) { - } - - @Override - public void windowIconified(WindowEvent e) { - } - - @Override - public void windowDeiconified(WindowEvent e) { - } - - @Override - public void windowActivated(WindowEvent e) { - if (projectFrame.isDisplayable()) { - projectFrame.setVisible(true); - } - anchor.dispose(); - } - - @Override - public void windowDeactivated(WindowEvent e) { - } - }); - anchor.pack(); - anchor.setVisible(true); - anchor.toFront(); - } - } - public interface PositionMapper { void onConnect(ScriptProvider provider, String remoteBaseUrl); diff --git a/src/io/flutter/widgetpreview/WidgetPreviewPanel.java b/src/io/flutter/widgetpreview/WidgetPreviewPanel.java index c016fd126e..21efbd12bd 100644 --- a/src/io/flutter/widgetpreview/WidgetPreviewPanel.java +++ b/src/io/flutter/widgetpreview/WidgetPreviewPanel.java @@ -8,9 +8,14 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.colors.EditorColorsListener; import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.fileChooser.FileChooser; import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.ui.SimpleToolWindowPanel; import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.util.messages.MessageBusConnection; import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; @@ -22,12 +27,14 @@ import io.flutter.devtools.DevToolsUtils; import io.flutter.logging.PluginLogger; import io.flutter.pub.PubRoot; +import io.flutter.pub.PubRootCache; import io.flutter.run.daemon.DevToolsInstance; import io.flutter.run.daemon.DevToolsService; import io.flutter.sdk.FlutterCommand; import io.flutter.sdk.FlutterSdk; import io.flutter.sdk.FlutterSdkVersion; import io.flutter.settings.FlutterSettings; +import io.flutter.utils.LabelInput; import io.flutter.utils.MostlySilentColoredProcessHandler; import io.flutter.utils.OpenApiUtils; import io.flutter.view.BrowserUrlProvider; @@ -71,10 +78,10 @@ public WidgetPreviewPanel(@NotNull Project project, @NotNull ToolWindow toolWind showInfoMessage(FlutterBundle.message("widget.preview.initializing")); // Start the preview process asynchronously - startWidgetPreview(); + startWidgetPreview(null); } - private void startWidgetPreview() { + private void startWidgetPreview(@Nullable VirtualFile file) { OpenApiUtils.safeExecuteOnPooledThread(() -> { try { // Check versioning of Flutter SDK. @@ -125,10 +132,10 @@ private void startWidgetPreview() { }); }); - final PubRoot root = PubRoot.forFile(project.getProjectFile()); + final PubRoot root = getPubRoot(file); if (root == null) { LOG.warn("Pub root not found for project: " + project.getName()); - showInfoMessage("Pub root could not be found to start widget preview."); + showRetryMessage(FlutterBundle.message("widget.preview.pubroot.not.found"), file); return; } @@ -150,6 +157,16 @@ private void startWidgetPreview() { }); } + private @Nullable PubRoot getPubRoot(@Nullable VirtualFile file) { + return OpenApiUtils.safeRunReadAction(() -> { + if (file != null) { + return PubRootCache.getInstance(project).getRoot(file); + } else { + return PubRoot.forFile(project.getProjectFile()); + } + }); + } + private @Nullable String getDevToolsUri() { try { final CompletableFuture devToolsFuture = DevToolsService.getInstance(project).getDevToolsInstance(); @@ -252,6 +269,34 @@ private void showInfoMessage(@NotNull String message) { }); } + private void showRetryMessage(@NotNull String message, @Nullable VirtualFile file) { + ApplicationManager.getApplication().invokeLater(() -> { + final List inputs = List.of( + new LabelInput(message), + new LabelInput(FlutterBundle.message("widget.preview.retry"), (label, data) -> startWidgetPreview(file)), + new LabelInput(FlutterBundle.message("widget.preview.choose_pubroot"), (label, data) -> chooseFile()) + ); + final JPanel panel = viewUtils.createClickableLabelPanel(inputs); + contentPanel.removeAll(); + contentPanel.add(panel, BorderLayout.CENTER); + contentPanel.revalidate(); + contentPanel.repaint(); + }); + } + + private void chooseFile() { + final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.singleFileOrDir() + .withTitle(FlutterBundle.message("widget.preview.choose_pubroot.title")) + .withDescription(FlutterBundle.message("widget.preview.choose_pubroot.description")) + .withRoots(FlutterUtils.getProjectRoot(project)); + + final VirtualFile selectedFile = FileChooser.chooseFile(descriptor, project, null); + if (selectedFile != null + && OpenApiUtils.safeRunReadAction(() -> ProjectFileIndex.getInstance(project).isInContent(selectedFile))) { + startWidgetPreview(selectedFile); + } + } + @Override public void dispose() { // Terminate the Flutter process when the tool window is closed @@ -264,7 +309,12 @@ public void dispose() { // Dispose the browser tab final EmbeddedTab tab = browserTabRef.getAndSet(null); if (tab != null) { - tab.close(); + try { + tab.close(); + } + catch (Exception ex) { + LOG.info(ex); + } } } } diff --git a/testSrc/unit/io/flutter/run/common/CommonTestConfigUtilsTest.java b/testSrc/unit/io/flutter/run/common/CommonTestConfigUtilsTest.java new file mode 100644 index 0000000000..2c0bd768fe --- /dev/null +++ b/testSrc/unit/io/flutter/run/common/CommonTestConfigUtilsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 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.run.common; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.jetbrains.lang.dart.psi.DartCallExpression; +import io.flutter.AbstractDartElementTest; +import io.flutter.dart.DartSyntax; +import io.flutter.run.test.TestConfigUtils; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class CommonTestConfigUtilsTest extends AbstractDartElementTest { + @Test + public void extractTestNameShouldNotEscapeNonAscii() throws Exception { + run(() -> { + final PsiElement testKeyword = setUpDartElement( + "main() { test('テスト', () {}); }", "test", LeafPsiElement.class); + final DartCallExpression call = + DartSyntax.findEnclosingFunctionCall(testKeyword, "test"); + assertNotNull(call); + + final String name = TestConfigUtils.getInstance().extractTestName(call); + assertEquals("テスト", name); + }); + } +} diff --git a/testSrc/unit/io/flutter/sdk/FlutterSdkVersionTest.java b/testSrc/unit/io/flutter/sdk/FlutterSdkVersionTest.java index 0a288e3105..a72f2f39c1 100644 --- a/testSrc/unit/io/flutter/sdk/FlutterSdkVersionTest.java +++ b/testSrc/unit/io/flutter/sdk/FlutterSdkVersionTest.java @@ -18,18 +18,19 @@ public void parsesGoodVersion() { @Test public void trackSdkVersionSupport() { - assertFalse(new FlutterSdkVersion("3.15.0").isSDKSupported()); - assertFalse(new FlutterSdkVersion("3.16.0-0.1.pre").isSDKSupported()); - assertTrue(new FlutterSdkVersion("3.16.0").isSDKSupported()); - assertTrue(new FlutterSdkVersion("3.16.1").isSDKSupported()); - assertTrue(new FlutterSdkVersion("3.17.0").isSDKSupported()); + assertFalse(new FlutterSdkVersion("3.19.3").isSDKSupported()); + assertFalse(new FlutterSdkVersion("3.19.4-0.1.pre").isSDKSupported()); + assertTrue(new FlutterSdkVersion("3.19.4").isSDKSupported()); + assertTrue(new FlutterSdkVersion("3.19.5").isSDKSupported()); + assertTrue(new FlutterSdkVersion("3.20.0").isSDKSupported()); assertFalse(new FlutterSdkVersion("unknown").isSDKSupported()); - assertTrue(new FlutterSdkVersion("3.16.0").isSDKAboutToSunset()); - assertTrue(new FlutterSdkVersion("3.17.0").isSDKAboutToSunset()); - assertTrue(new FlutterSdkVersion("3.19.3").isSDKAboutToSunset()); - assertFalse(new FlutterSdkVersion("3.19.4").isSDKAboutToSunset()); - assertFalse(new FlutterSdkVersion("3.20.0").isSDKAboutToSunset()); + assertTrue(new FlutterSdkVersion("3.19.4").isSDKAboutToSunset()); + assertTrue(new FlutterSdkVersion("3.20.0").isSDKAboutToSunset()); + assertTrue(new FlutterSdkVersion("3.22.0").isSDKAboutToSunset()); + assertTrue(new FlutterSdkVersion("3.22.1").isSDKAboutToSunset()); + assertFalse(new FlutterSdkVersion("3.22.2").isSDKAboutToSunset()); + assertFalse(new FlutterSdkVersion("3.23.0").isSDKAboutToSunset()); } @Test diff --git a/tool/github.sh b/tool/github.sh index a17814ce8c..6e21407a13 100755 --- a/tool/github.sh +++ b/tool/github.sh @@ -135,6 +135,9 @@ elif [ "VERIFY_BOT" = "$BOT" ] ; then if [ $EXIT_STATUS -ne 0 ]; then echo -e "${RED}${BOLD}Build failed: New verification issues were detected.${NC}" + echo -e "${YELLOW}To update the baselines with these new issues, run:${NC}" + echo -e "${YELLOW} ./tool/update_baselines.sh${NC}" + echo -e "${YELLOW}from the repository root and commit the changes.${NC}" exit 1 fi diff --git a/tool/kokoro/deploy.sh b/tool/kokoro/deploy.sh index dfefc78c4a..9aac60ca14 100755 --- a/tool/kokoro/deploy.sh +++ b/tool/kokoro/deploy.sh @@ -10,6 +10,26 @@ echo "kokoro build start" echo "kokoro build finished" echo "kokoro deploy start" -./bin/plugin deploy --channel=dev + +KOKORO_TOKEN_FILE="${KOKORO_KEYSTORE_DIR}/${FLUTTER_KEYSTORE_ID}_${FLUTTER_KEYSTORE_NAME}" +if [ ! -f "$KOKORO_TOKEN_FILE" ]; then + echo "Error: Keystore token file not found at $KOKORO_TOKEN_FILE" + exit 1 +fi +TOKEN=$(cat "$KOKORO_TOKEN_FILE") + +ZIP_FILE="build/distributions/flutter-intellij-kokoro.zip" +if [ ! -f "$ZIP_FILE" ]; then + echo "Error: Zip file not found at $ZIP_FILE" + exit 1 +fi + +echo "Uploading $ZIP_FILE to JetBrains Marketplace..." +curl -if --fail \ + --header "Authorization: Bearer $TOKEN" \ + -F pluginId=9212 \ + -F file=@"$ZIP_FILE" \ + -F channel=dev \ + https://plugins.jetbrains.com/plugin/uploadPlugin echo "kokoro deploy finished" diff --git a/tool/update_baselines.sh b/tool/update_baselines.sh new file mode 100755 index 0000000000..8052bd039a --- /dev/null +++ b/tool/update_baselines.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Copyright 2026 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. + +if [ ! -f "build.gradle.kts" ]; then + echo "Error: This script must be run from the repository root directory." + exit 1 +fi + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' # None (Reset) + +echo -e "${BOLD}Running plugin verification...${NC}" + +rm -rf build/reports/pluginVerifier + +VERSIONS=$(ls tool/baseline) + +if [ -z "$VERSIONS" ]; then + echo -e "${YELLOW}Warning: No baseline directories found in tool/baseline.${NC}" + exit 0 +fi + +echo -e "${BOLD}Found versions to update: $VERSIONS${NC}" + +for version in $VERSIONS; do + echo -e "${BOLD}Verifying version $version...${NC}" + ./gradlew verifyPlugin -PsingleIdeVersion=$version --no-configuration-cache --no-daemon || true + + echo -e "${BOLD}Processing baseline for $version...${NC}" + BASELINE="tool/baseline/$version/verifier-baseline.txt" + REPORT=$(find build/reports/pluginVerifier -path "*-$version.*/report.md" | head -n 1) + + if [ -f "$REPORT" ]; then + echo "Extracting issues from $REPORT" + mkdir -p "$(dirname "$BASELINE")" + grep "^*" "$REPORT" | sort > "$BASELINE" + echo -e "${GREEN}Updated baseline at $BASELINE${NC}" + else + echo -e "${YELLOW}Warning: Report does not exist for version $version. Skipping.${NC}" + fi +done + +echo -e "${BOLD}Done updating baselines.${NC}"