From 2be41ba607e21b949e81cc9efddc17ca6e02b931 Mon Sep 17 00:00:00 2001 From: lijianguo Date: Mon, 10 Nov 2025 17:23:51 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E9=80=82=E9=85=8D2025.1.7=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 64 +++++++++++++------ gradle.properties | 13 ++-- gradlew | 44 +++++++++---- gradlew.bat | 37 ++++++----- .../assemble/AssembleServiceExecutor.java | 23 +++++-- .../fudoc/apidoc/view/FuDocSettingForm.java | 4 +- .../wdf/fudoc/common/AbstractClassAction.java | 14 ++++ .../listener/FuDocSetupAbleListener.java | 22 ++++++- .../wdf/fudoc/compat/JsonFileTypeCompat.java | 46 +++++++++++++ .../wdf/fudoc/components/FuTabComponent.java | 3 +- .../factory/LightVirtualFileFactory.java | 4 +- .../message/FuHighlightComponent.java | 6 +- .../components/message/MessageComponent.java | 6 +- .../manager/FuRequestConsoleManager.java | 6 +- .../tab/request/HttpRequestBodyTab.java | 4 +- .../request/tab/request/ResponseTabView.java | 4 +- .../request/view/FuRequestStatusInfoView.java | 9 ++- .../com/wdf/fudoc/test/action/TestAction.java | 3 +- .../action/editor/FuEditorFormatAction.java | 4 +- .../wdf/fudoc/test/view/TestHtmlPanel.java | 3 +- .../wdf/fudoc/test/view/TestRequestFrom.java | 4 +- .../com/wdf/fudoc/test/view/TestView1.java | 11 ++-- .../wdf/fudoc/test/view/ToolBarTestForm.java | 7 +- .../java/com/wdf/fudoc/util/EditorUtils.java | 4 +- .../java/com/wdf/fudoc/util/StorageUtils.java | 3 +- .../java/com/wdf/fudoc/util/ToolBarUtils.java | 6 +- src/main/resources/META-INF/plugin.xml | 9 +-- .../{MyPluginTest.kt => MyPluginTest.kt.bak} | 3 +- 29 files changed, 261 insertions(+), 106 deletions(-) create mode 100644 src/main/java/com/wdf/fudoc/compat/JsonFileTypeCompat.java rename src/test/kotlin/com/github/wangdingfu/fuapidocplugin/{MyPluginTest.kt => MyPluginTest.kt.bak} (89%) diff --git a/.gitignore b/.gitignore index 9c914506..64ccdf2b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .qodana build /src/main/java/com/wdf/fudoc/start/ +.intellijPlatform/ diff --git a/build.gradle.kts b/build.gradle.kts index 6ad8a926..7a636159 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,11 +8,11 @@ plugins { // Java support id("java") // Kotlin support - id("org.jetbrains.kotlin.jvm") version "1.6.10" - // Gradle IntelliJ Plugin - id("org.jetbrains.intellij") version "1.13.3" + id("org.jetbrains.kotlin.jvm") version "2.0.21" + // Gradle IntelliJ Platform Plugin 2.x - using 2.0.1 to avoid runIde bug in 2.1.0 + id("org.jetbrains.intellij.platform") version "2.10.4" // Gradle Changelog Plugin - id("org.jetbrains.changelog") version "1.3.1" + id("org.jetbrains.changelog") version "2.2.1" } @@ -27,11 +27,16 @@ repositories { setUrl("https://maven.aliyun.com/nexus/content/groups/public/") } mavenCentral() + + // IntelliJ Platform repositories + intellijPlatform { + defaultRepositories() + } } dependencies { - compileOnly("org.projectlombok:lombok:1.18.26") - annotationProcessor("org.projectlombok:lombok:1.18.26") + compileOnly("org.projectlombok:lombok:1.18.34") + annotationProcessor("org.projectlombok:lombok:1.18.34") implementation("com.github.jsonzou:jmockdata:4.3.0") implementation("org.freemarker:freemarker:2.3.31") implementation("cn.hutool:hutool-json:5.8.20") @@ -42,22 +47,31 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.10") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("com.atlassian.commonmark:commonmark:0.17.0") - implementation("cn.fudoc:fu-api-commons:${properties["ideaVersion"]}.${properties["fudocVersion"]}") -} + implementation("cn.fudoc:fu-api-commons:222.${properties["fudocVersion"]}") + // IntelliJ Platform dependencies + intellijPlatform { + create(properties("platformType"), properties("platformVersion")) -// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin -intellij { - pluginName.set(properties("pluginName")) - version.set(properties("platformVersion")) - type.set(properties("platformType")) - updateSinceUntilBuild.set(false) + // Bundled plugins (plugins that come with IntelliJ) + bundledPlugins(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) - //沙箱目录位置,用于保存IDEA的设置,默认在build文件下面,防止clean,放在根目录下。 - sandboxDir.set("${rootProject.rootDir}/idea-sandbox") + // IntelliJ Platform instrumentation + instrumentationTools() - // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) + // Test Framework + testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Platform) + } +} + +// Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html +intellijPlatform { + pluginConfiguration { + name.set(properties("pluginName")) + } + + // 沙箱目录位置,用于保存 IDEA 的设置,默认在 build 文件下面,防止 clean,放在根目录下。 + sandboxContainer.set(file("${rootProject.rootDir}/idea-sandbox")) } @@ -77,7 +91,9 @@ tasks { options.encoding = "UTF-8" } withType { - kotlinOptions.jvmTarget = it + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(it)) + } } } @@ -85,7 +101,17 @@ tasks { gradleVersion = properties("gradleVersion") } + // Configure runIde task to avoid IndexOutOfBoundsException + runIde { + maxHeapSize = "2g" + jvmArgs = listOf( + "-Xms512m" + ) + } + patchPluginXml { + sinceBuild.set("251") + untilBuild.set("") // Extract the section from README.md and provide for the plugin's manifest pluginDescription.set( diff --git a/gradle.properties b/gradle.properties index f5f3c21c..28dc380d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,11 @@ pluginGroup = com.wdf.apidoc pluginName = FuDoc # SemVer format -> https://semver.org -pluginVersion = 222.1.8.9 +pluginVersion = 251.1.8.9 # \u63D2\u4EF6\u652F\u6301\u7248\u672C -ideaVersion = 222 +ideaVersion = 251 fudocVersion = 1.8 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html @@ -16,18 +16,19 @@ fudocVersion = 1.8 # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties platformType = IU -platformVersion = 2022.3.1 +platformVersion = 2025.1.5 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 +# Note: 'json' is bundled into platform core in 2025.1+, removed from plugin list platformPlugins = com.intellij.java,JavaScript,com.jetbrains.restClient,org.jetbrains.idea.maven #platformPlugins = com.intellij.java,com.intellij.spring -# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 -javaVersion = 17 +# Java language level used to compile sources and to generate the files for - Java 21 is required for IDEA 2025.1 +javaVersion = 21 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 7.4 +gradleVersion = 8.13 # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. diff --git a/gradlew b/gradlew index 1b6c7873..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/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/. @@ -80,13 +82,12 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +217,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @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 +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/src/main/java/com/wdf/fudoc/apidoc/assemble/AssembleServiceExecutor.java b/src/main/java/com/wdf/fudoc/apidoc/assemble/AssembleServiceExecutor.java index c2b9a3fc..92bbe720 100644 --- a/src/main/java/com/wdf/fudoc/apidoc/assemble/AssembleServiceExecutor.java +++ b/src/main/java/com/wdf/fudoc/apidoc/assemble/AssembleServiceExecutor.java @@ -19,15 +19,24 @@ */ public class AssembleServiceExecutor { - private static final List SERVICE_LIST = Lists.newArrayList( - ServiceHelper.getService(ControllerAssembleService.class), - ServiceHelper.getService(FeignAssembleService.class), - ServiceHelper.getService(InterfaceAssembleService.class) - ); + /** + * 懒加载服务列表 + * 不在类初始化时加载,而是在第一次使用时加载 + * 这样避免了类加载时就依赖 IntelliJ Platform 的服务容器 + * + * @return 组装服务列表 + */ + private static List getServiceList() { + return Lists.newArrayList( + ServiceHelper.getService(ControllerAssembleService.class), + ServiceHelper.getService(FeignAssembleService.class), + ServiceHelper.getService(InterfaceAssembleService.class) + ); + } public static List execute(FuDocContext fuDocContext, ClassInfoDesc classInfoDesc) { - for (FuDocAssembleService fuDocAssembleService : SERVICE_LIST) { + for (FuDocAssembleService fuDocAssembleService : getServiceList()) { if (fuDocAssembleService.isAssemble(fuDocContext, classInfoDesc)) { return fuDocAssembleService.assemble(fuDocContext, classInfoDesc); } @@ -37,7 +46,7 @@ public static List execute(FuDocContext fuDocContext, ClassInfoDe public static List executeByRequest(FuDocContext fuDocContext, ClassInfoDesc classInfoDesc) { - for (FuDocAssembleService fuDocAssembleService : SERVICE_LIST) { + for (FuDocAssembleService fuDocAssembleService : getServiceList()) { if (fuDocAssembleService.isAssemble(fuDocContext, classInfoDesc)) { return fuDocAssembleService.requestAssemble(fuDocContext, classInfoDesc); } diff --git a/src/main/java/com/wdf/fudoc/apidoc/view/FuDocSettingForm.java b/src/main/java/com/wdf/fudoc/apidoc/view/FuDocSettingForm.java index df1e76e5..50554eed 100644 --- a/src/main/java/com/wdf/fudoc/apidoc/view/FuDocSettingForm.java +++ b/src/main/java/com/wdf/fudoc/apidoc/view/FuDocSettingForm.java @@ -2,7 +2,7 @@ import cn.hutool.json.JSONUtil; import com.intellij.ide.highlighter.XmlFileType; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -144,7 +144,7 @@ private void createUIComponents() { this.objectEditorComponent = FuEditorComponent.create(XmlFileType.INSTANCE, null,disposable); this.enum1EditorComponent = FuEditorComponent.create(XmlFileType.INSTANCE, null,disposable); this.enum2EditorComponent = FuEditorComponent.create(XmlFileType.INSTANCE, null,disposable); - this.settingEditorComponent = FuEditorComponent.create(JsonFileType.INSTANCE, null,disposable); + this.settingEditorComponent = FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(), null,disposable); this.yapiEditorComponent = FuEditorComponent.create(XmlFileType.INSTANCE, null,disposable); //初始化面板 diff --git a/src/main/java/com/wdf/fudoc/common/AbstractClassAction.java b/src/main/java/com/wdf/fudoc/common/AbstractClassAction.java index 747f94fb..c6960f64 100644 --- a/src/main/java/com/wdf/fudoc/common/AbstractClassAction.java +++ b/src/main/java/com/wdf/fudoc/common/AbstractClassAction.java @@ -1,5 +1,6 @@ package com.wdf.fudoc.common; +import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.Presentation; @@ -52,6 +53,19 @@ protected String exceptionMsg() { protected abstract FuDocAction getAction(); + /** + * 指定 Action 的线程模型 + * 从 IDEA 2022.3+ 开始,必须显式声明 ActionUpdateThread + * BGT (Background Thread) 表示在后台线程执行 update() 方法,避免阻塞 EDT + * 这样可以安全地在 update() 中访问 PSI 数据 + * + * @return ActionUpdateThread.BGT - 后台线程模型 + */ + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + /** * 执行动作 * diff --git a/src/main/java/com/wdf/fudoc/common/listener/FuDocSetupAbleListener.java b/src/main/java/com/wdf/fudoc/common/listener/FuDocSetupAbleListener.java index cd5df4d4..8a9c4cd0 100644 --- a/src/main/java/com/wdf/fudoc/common/listener/FuDocSetupAbleListener.java +++ b/src/main/java/com/wdf/fudoc/common/listener/FuDocSetupAbleListener.java @@ -1,23 +1,39 @@ package com.wdf.fudoc.common.listener; import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.StartupActivity; +import com.intellij.openapi.startup.ProjectActivity; import cn.fudoc.common.service.FuDocSetupAble; +import kotlin.Unit; +import kotlin.coroutines.Continuation; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ServiceLoader; /** + * Fu Doc 项目初始化监听器 + * 从 StartupActivity 迁移到 ProjectActivity (IDEA 2022.3+ 推荐) + * * @author wangdingfu * @date 2023-10-02 18:59:04 */ @Slf4j -public class FuDocSetupAbleListener implements StartupActivity { +public class FuDocSetupAbleListener implements ProjectActivity { + /** + * 项目启动后执行 + * 这是新的 ProjectActivity API,在协程上下文中执行,不阻塞 UI 线程 + * + * @param project 当前项目 + * @param continuation Kotlin 协程上下文 (Java 调用时可忽略) + * @return null (Java 调用时返回 null 即可) + */ + @Nullable @Override - public void runActivity(@NotNull Project project) { + public Object execute(@NotNull Project project, @NotNull Continuation continuation) { ServiceLoader load = ServiceLoader.load(FuDocSetupAble.class, FuDocSetupAbleListener.class.getClassLoader()); load.forEach(f -> f.init(project)); + return null; } } diff --git a/src/main/java/com/wdf/fudoc/compat/JsonFileTypeCompat.java b/src/main/java/com/wdf/fudoc/compat/JsonFileTypeCompat.java new file mode 100644 index 00000000..dfdec789 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/compat/JsonFileTypeCompat.java @@ -0,0 +1,46 @@ +package com.wdf.fudoc.compat; + +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeManager; + +/** + * JsonFileType 兼容类 + * 用于兼容 IDEA 2025+ 版本中 JsonFileType 的变化 + * + * @author wangdingfu + * @date 2025-01-10 + */ +public class JsonFileTypeCompat { + + /** + * 获取 JSON FileType 实例 + * 兼容不同版本的 IDEA + */ + public static FileType getJsonFileType() { + try { + // 尝试使用反射获取 JsonFileType.INSTANCE (兼容旧版本) + Class jsonFileTypeClass = Class.forName("com.intellij.json.JsonFileType"); + java.lang.reflect.Field instanceField = jsonFileTypeClass.getDeclaredField("INSTANCE"); + return (FileType) instanceField.get(null); + } catch (Exception e) { + // 如果反射失败,使用 FileTypeManager (IDEA 2025+) + FileType fileType = FileTypeManager.getInstance().findFileTypeByName("JSON"); + if (fileType != null) { + return fileType; + } + // 最后的fallback + return FileTypeManager.getInstance().getFileTypeByExtension("json"); + } + } + + /** + * 判断是否为 JSON 文件类型 + */ + public static boolean isJsonFileType(FileType fileType) { + if (fileType == null) { + return false; + } + FileType jsonType = getJsonFileType(); + return jsonType != null && jsonType.equals(fileType); + } +} diff --git a/src/main/java/com/wdf/fudoc/components/FuTabComponent.java b/src/main/java/com/wdf/fudoc/components/FuTabComponent.java index de92ebef..961242d6 100644 --- a/src/main/java/com/wdf/fudoc/components/FuTabComponent.java +++ b/src/main/java/com/wdf/fudoc/components/FuTabComponent.java @@ -9,7 +9,8 @@ import com.wdf.fudoc.components.listener.TabBarListener; import com.wdf.fudoc.util.ToolBarUtils; import icons.FuDocIcons; -import k.p.D; +// TODO: 升级到2025后需要更新fu-api-commons:251.1.8 +// import k.p.D; import lombok.Getter; import lombok.Setter; import org.apache.commons.collections.CollectionUtils; diff --git a/src/main/java/com/wdf/fudoc/components/factory/LightVirtualFileFactory.java b/src/main/java/com/wdf/fudoc/components/factory/LightVirtualFileFactory.java index 04f27d1d..ed2cd596 100644 --- a/src/main/java/com/wdf/fudoc/components/factory/LightVirtualFileFactory.java +++ b/src/main/java/com/wdf/fudoc/components/factory/LightVirtualFileFactory.java @@ -2,7 +2,7 @@ import com.intellij.ide.highlighter.JavaFileType; import com.intellij.ide.highlighter.XmlFileType; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.fileTypes.FileType; import com.intellij.testFramework.LightVirtualFile; @@ -32,7 +32,7 @@ public static LightVirtualFile create(FileType fileType) { if (fileType instanceof JavaFileType) { return new LightVirtualFile(FILE_NAME + JAVA_SUFFIX); } - if (fileType instanceof JsonFileType) { + if (JsonFileTypeCompat.isJsonFileType(fileType)) { return new LightVirtualFile(FILE_NAME + JSON_SUFFIX); } } diff --git a/src/main/java/com/wdf/fudoc/components/message/FuHighlightComponent.java b/src/main/java/com/wdf/fudoc/components/message/FuHighlightComponent.java index 55b5e906..8f66489a 100644 --- a/src/main/java/com/wdf/fudoc/components/message/FuHighlightComponent.java +++ b/src/main/java/com/wdf/fudoc/components/message/FuHighlightComponent.java @@ -41,7 +41,11 @@ public class FuHighlightComponent extends JComponent implements Accessible { @Override public void updateUI() { - GraphicsUtil.setAntialiasingType(this, AntialiasingType.getAAHintForSwingComponent()); + super.updateUI(); + // Note: AntialiasingType.getAAHintForSwingComponent() is deprecated and removed in IDEA 2025.1+ + // Use UISettings to get the current antialiasing type instead + AntialiasingType aaType = UISettings.getInstance().getIdeAAType(); + GraphicsUtil.setAntialiasingType(this, aaType); } /** diff --git a/src/main/java/com/wdf/fudoc/components/message/MessageComponent.java b/src/main/java/com/wdf/fudoc/components/message/MessageComponent.java index fce47cbb..5a46c3b0 100644 --- a/src/main/java/com/wdf/fudoc/components/message/MessageComponent.java +++ b/src/main/java/com/wdf/fudoc/components/message/MessageComponent.java @@ -1,7 +1,8 @@ package com.wdf.fudoc.components.message; import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.wm.impl.status.MemoryUsagePanel; +// TODO: IDEA 2025 MemoryUsagePanel API变更 +// import com.intellij.openapi.wm.impl.status.MemoryUsagePanel; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import cn.fudoc.common.msg.bo.FuMsgBO; @@ -132,7 +133,8 @@ private void initRightPanel() { public void layoutContainer(Container target) { super.layoutContainer(target); for (Component component : target.getComponents()) { - if (component instanceof MemoryUsagePanel) { + // TODO: IDEA 2025 MemoryUsagePanel API变更 + if (false && component.getClass().getSimpleName().equals("MemoryUsagePanel")) { Rectangle r = component.getBounds(); r.y = 0; r.width += SystemInfo.isMac ? 4 : 0; diff --git a/src/main/java/com/wdf/fudoc/request/manager/FuRequestConsoleManager.java b/src/main/java/com/wdf/fudoc/request/manager/FuRequestConsoleManager.java index 67e0f4f8..0c285704 100644 --- a/src/main/java/com/wdf/fudoc/request/manager/FuRequestConsoleManager.java +++ b/src/main/java/com/wdf/fudoc/request/manager/FuRequestConsoleManager.java @@ -9,7 +9,7 @@ import com.intellij.execution.impl.ConsoleViewUtil; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.httpClient.http.request.HttpRequestFileType; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.wdf.fudoc.common.FuDocRender; import com.wdf.fudoc.common.base.KeyValueBO; import com.wdf.fudoc.common.constant.FuConsoleConstants; @@ -110,7 +110,7 @@ private static void logRequest(FuLogger fuLogger, HttpRequest httpRequest) { ConsoleViewUtil.printAsFileType(fuLogger.getConsoleView(), FuDocRender.render(requestConsoleData, "console/request_console.ftl"), HttpRequestFileType.INSTANCE); String bodyContent = buildRequestBody(httpRequest); if (JSONUtil.isTypeJSON(bodyContent)) { - ConsoleViewUtil.printAsFileType(fuLogger.getConsoleView(), bodyContent, JsonFileType.INSTANCE); + ConsoleViewUtil.printAsFileType(fuLogger.getConsoleView(), bodyContent, JsonFileTypeCompat.getJsonFileType()); } else { fuLogger.info(bodyContent); } @@ -136,7 +136,7 @@ private static void logResponse(FuLogger fuLogger, HttpResponse httpResponse) { return; } if (JSONUtil.isTypeJSON(bodyContent)) { - ConsoleViewUtil.printAsFileType(fuLogger.getConsoleView(), JSONUtil.toJsonPrettyStr(bodyContent), JsonFileType.INSTANCE); + ConsoleViewUtil.printAsFileType(fuLogger.getConsoleView(), JSONUtil.toJsonPrettyStr(bodyContent), JsonFileTypeCompat.getJsonFileType()); fuLogger.println(); } else { fuLogger.info(bodyContent); diff --git a/src/main/java/com/wdf/fudoc/request/tab/request/HttpRequestBodyTab.java b/src/main/java/com/wdf/fudoc/request/tab/request/HttpRequestBodyTab.java index d3fa0acf..990230d4 100644 --- a/src/main/java/com/wdf/fudoc/request/tab/request/HttpRequestBodyTab.java +++ b/src/main/java/com/wdf/fudoc/request/tab/request/HttpRequestBodyTab.java @@ -1,7 +1,7 @@ package com.wdf.fudoc.request.tab.request; import com.google.common.collect.Lists; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.ui.tabs.TabInfo; @@ -60,7 +60,7 @@ public HttpRequestBodyTab(Disposable disposable) { this.urlencodedComponent = FuTableComponent.createKeyValue(); this.urlencodedPanel = this.urlencodedComponent.createPanel(); this.rawComponent = FuEditorComponent.create(PlainTextFileType.INSTANCE,disposable); - this.jsonComponent = FuEditorComponent.create(JsonFileType.INSTANCE,disposable); + this.jsonComponent = FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(),disposable); this.binaryComponent = new JPanel(); this.formDataEditorComponent = FuEditorComponent.create(PlainTextFileType.INSTANCE,disposable); this.urlencodedEditorComponent = FuEditorComponent.create(PlainTextFileType.INSTANCE,disposable); diff --git a/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java b/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java index 546508f5..ed6b643e 100644 --- a/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java +++ b/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java @@ -3,7 +3,7 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.http.HttpResponse; import cn.hutool.json.JSONUtil; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.ui.tabs.TabInfo; @@ -56,7 +56,7 @@ public ResponseTabView(Project project, JPanel slidePanel, Disposable disposable this.slidePanel = slidePanel; this.responseErrorView = new ResponseErrorView(disposable); this.responseFileView = new ResponseFileView(); - this.fuEditorComponent = FuEditorComponent.create(JsonFileType.INSTANCE, "", disposable); + this.fuEditorComponent = FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(), "", disposable); this.rootPanel = new JPanel(new BorderLayout()); switchPanel(1, this.fuEditorComponent.getMainPanel()); } diff --git a/src/main/java/com/wdf/fudoc/request/view/FuRequestStatusInfoView.java b/src/main/java/com/wdf/fudoc/request/view/FuRequestStatusInfoView.java index 1c048ed6..e5cf433f 100644 --- a/src/main/java/com/wdf/fudoc/request/view/FuRequestStatusInfoView.java +++ b/src/main/java/com/wdf/fudoc/request/view/FuRequestStatusInfoView.java @@ -3,7 +3,8 @@ import com.google.common.collect.Lists; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.wm.impl.status.MemoryUsagePanel; +// TODO: IDEA 2025 MemoryUsagePanel API变更 +// import com.intellij.openapi.wm.impl.status.MemoryUsagePanel; import com.intellij.util.ui.JBUI; import com.wdf.fudoc.components.widget.FuWidget; import com.wdf.fudoc.request.pojo.FuHttpRequestData; @@ -95,7 +96,8 @@ private void initRightPanel() { public void layoutContainer(Container target) { super.layoutContainer(target); for (Component component : target.getComponents()) { - if (component instanceof MemoryUsagePanel) { + // TODO: IDEA 2025 MemoryUsagePanel API变更 + if (false && component.getClass().getSimpleName().equals("MemoryUsagePanel")) { Rectangle r = component.getBounds(); r.y = 0; r.width += SystemInfo.isMac ? 4 : 0; @@ -119,7 +121,8 @@ private void initLeftPanel() { public void layoutContainer(Container target) { super.layoutContainer(target); for (Component component : target.getComponents()) { - if (component instanceof MemoryUsagePanel) { + // TODO: IDEA 2025 MemoryUsagePanel API变更 + if (false && component.getClass().getSimpleName().equals("MemoryUsagePanel")) { Rectangle r = component.getBounds(); r.y = 0; r.width += SystemInfo.isMac ? 4 : 0; diff --git a/src/main/java/com/wdf/fudoc/test/action/TestAction.java b/src/main/java/com/wdf/fudoc/test/action/TestAction.java index 5d446ff4..b85fa642 100644 --- a/src/main/java/com/wdf/fudoc/test/action/TestAction.java +++ b/src/main/java/com/wdf/fudoc/test/action/TestAction.java @@ -104,7 +104,8 @@ private void request(AnActionEvent e) { } } RequestBuilder requestBuilder = new RestClientRequestBuilder(); - HttpRequestConfig requestConfig = HttpRequestPsiConverter.toRequestConfig(firstRequest); + // TODO: IDEA 2025 HttpRequestPsiConverter API变更 + // HttpRequestConfig requestConfig = HttpRequestPsiConverter.toRequestConfig(firstRequest); // try { // RestClientRequest restClientRequest = HttpRequestPsiConverter.convertFromHttpRequest(firstRequest, substitutor, requestBuilder); // CurlCopyPastePreProcessor preProcessor = new CurlCopyPastePreProcessor(); diff --git a/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java b/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java index ace4a72e..2d3bbb83 100644 --- a/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java +++ b/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java @@ -1,6 +1,6 @@ package com.wdf.fudoc.test.action.editor; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Editor; @@ -38,7 +38,7 @@ public void update(@NotNull AnActionEvent e) { if (editor != null && e.getProject() != null) { PsiFile file = PsiDocumentManager.getInstance(e.getProject()).getPsiFile(editor.getDocument()); if (Objects.nonNull(file) && FuStringUtils.isNotBlank(file.getName())) { - e.getPresentation().setEnabledAndVisible(file.getName().equals(FuDocConstants.FU_DOC_FILE + JsonFileType.DEFAULT_EXTENSION)); + e.getPresentation().setEnabledAndVisible(file.getName().equals(FuDocConstants.FU_DOC_FILE + "json")); } } else { e.getPresentation().setEnabledAndVisible(false); diff --git a/src/main/java/com/wdf/fudoc/test/view/TestHtmlPanel.java b/src/main/java/com/wdf/fudoc/test/view/TestHtmlPanel.java index 439cb0c5..0337676d 100644 --- a/src/main/java/com/wdf/fudoc/test/view/TestHtmlPanel.java +++ b/src/main/java/com/wdf/fudoc/test/view/TestHtmlPanel.java @@ -6,7 +6,8 @@ import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ui.HTMLEditorKitBuilder; -import com.intellij.util.ui.HtmlPanel; +// TODO: IDEA 2025中HtmlPanel API变更 +// import com.intellij.util.ui.HtmlPanel; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/wdf/fudoc/test/view/TestRequestFrom.java b/src/main/java/com/wdf/fudoc/test/view/TestRequestFrom.java index f6cd4394..83918e49 100644 --- a/src/main/java/com/wdf/fudoc/test/view/TestRequestFrom.java +++ b/src/main/java/com/wdf/fudoc/test/view/TestRequestFrom.java @@ -1,7 +1,7 @@ package com.wdf.fudoc.test.view; import com.google.common.collect.Lists; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.ui.GuiUtils; @@ -116,7 +116,7 @@ private JPanel createTable1Panel() { } private JPanel createEditorPanel() { - return FuEditorComponent.create(JsonFileType.INSTANCE, "",this).getMainPanel(); + return FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(), "",this).getMainPanel(); } @Override diff --git a/src/main/java/com/wdf/fudoc/test/view/TestView1.java b/src/main/java/com/wdf/fudoc/test/view/TestView1.java index 68567861..93f1b2aa 100644 --- a/src/main/java/com/wdf/fudoc/test/view/TestView1.java +++ b/src/main/java/com/wdf/fudoc/test/view/TestView1.java @@ -3,7 +3,7 @@ import com.google.common.collect.Lists; import com.intellij.find.editorHeaderActions.Utils; import com.intellij.icons.AllIcons; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; @@ -73,9 +73,9 @@ public TestView1(Project project) { private void createUIComponents() { final JBTabsImpl tabs = new JBTabsImpl(ProjectUtils.getCurrProject()); initToolbar(); - tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileType.INSTANCE, "",this).getMainPanel()).setText("Body")); - tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileType.INSTANCE,"",this).getMainPanel()).setText("Params")); - tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileType.INSTANCE,"",this).getMainPanel()).setText("Header").setSideComponent(this.toolBarPanel)); + tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(), "",this).getMainPanel()).setText("Body")); + tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(),"",this).getMainPanel()).setText("Params")); + tabs.addTab(new TabInfo(FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(),"",this).getMainPanel()).setText("Header").setSideComponent(this.toolBarPanel)); this.topPanel = new BorderLayoutPanel(); this.topPanel.add(tabs.getComponent(),BorderLayout.CENTER); this.centerPanel = FuTableComponent.create(FuTableColumnFactory.keyValueColumns(), Lists.newArrayList(), KeyValueTableBO.class).createPanel(); @@ -118,7 +118,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { .createActionToolbar("FuRequestToolBar", actionGroup, true); toolbar.setTargetComponent(toolBarPanel); toolbar.setForceMinimumSize(true); - toolbar.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); + // Note: setLayoutPolicy() is deprecated and removed in IDEA 2025.1+ + // The default behavior is already NOWRAP, so this call is not needed Utils.setSmallerFontForChildren(toolbar); toolBarPanel.add(toolbar.getComponent(), BorderLayout.EAST); } diff --git a/src/main/java/com/wdf/fudoc/test/view/ToolBarTestForm.java b/src/main/java/com/wdf/fudoc/test/view/ToolBarTestForm.java index d2039858..735091de 100644 --- a/src/main/java/com/wdf/fudoc/test/view/ToolBarTestForm.java +++ b/src/main/java/com/wdf/fudoc/test/view/ToolBarTestForm.java @@ -3,7 +3,7 @@ import com.google.common.collect.Lists; import com.intellij.find.editorHeaderActions.Utils; import com.intellij.icons.AllIcons; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; @@ -65,7 +65,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { .createActionToolbar("FuRequestToolBar", actionGroup, true); toolbar.setTargetComponent(toolBarPanel); toolbar.setForceMinimumSize(true); - toolbar.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); + // Note: setLayoutPolicy() is deprecated and removed in IDEA 2025.1+ + // The default behavior is already NOWRAP, so this call is not needed Utils.setSmallerFontForChildren(toolbar); bulkEditBarPanel.add(toolbar.getComponent(), BorderLayout.EAST); return bulkEditBarPanel; @@ -96,7 +97,7 @@ private TabInfo createTabInfo(String title, Icon icon, JComponent component) { private void createUIComponents() { this.tablePanel = FuTableComponent.create(FuTableColumnFactory.keyValueColumns(), Lists.newArrayList(), KeyValueTableBO.class).createPanel(); - this.editPanel = FuEditorComponent.create(JsonFileType.INSTANCE, "",this).getMainPanel(); + this.editPanel = FuEditorComponent.create(JsonFileTypeCompat.getJsonFileType(), "",this).getMainPanel(); this.bulkEditPanel = createBulkEditBar(); this.toolBarPanel = new BorderLayoutPanel(); this.toolBarPanel.add(createTabPanel(),BorderLayout.CENTER); diff --git a/src/main/java/com/wdf/fudoc/util/EditorUtils.java b/src/main/java/com/wdf/fudoc/util/EditorUtils.java index ec42c729..9a83161b 100644 --- a/src/main/java/com/wdf/fudoc/util/EditorUtils.java +++ b/src/main/java/com/wdf/fudoc/util/EditorUtils.java @@ -1,6 +1,6 @@ package com.wdf.fudoc.util; -import com.intellij.json.JsonFileType; +import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.fileTypes.FileType; @@ -25,7 +25,7 @@ public static Document createDocument(String value, FileType fileType) { } public static Document createDocument(String value, FileType fileType, Project project) { - if (Objects.nonNull(fileType) && JsonFileType.INSTANCE.equals(fileType)) { + if (JsonFileTypeCompat.isJsonFileType(fileType)) { //只有json格式才创建虚拟文件 if (project == null) { project = ProjectUtils.getCurrProject(); diff --git a/src/main/java/com/wdf/fudoc/util/StorageUtils.java b/src/main/java/com/wdf/fudoc/util/StorageUtils.java index a00b2e14..c12da1d3 100644 --- a/src/main/java/com/wdf/fudoc/util/StorageUtils.java +++ b/src/main/java/com/wdf/fudoc/util/StorageUtils.java @@ -11,7 +11,8 @@ import com.intellij.psi.*; import cn.fudoc.common.util.JsonUtil; import com.wdf.fudoc.request.http.FuRequest; -import k.K.E; +// TODO: 升级到2025后需要更新fu-api-commons:251.1.8 +// import k.K.E; import lombok.extern.slf4j.Slf4j; import com.wdf.fudoc.util.FuStringUtils; diff --git a/src/main/java/com/wdf/fudoc/util/ToolBarUtils.java b/src/main/java/com/wdf/fudoc/util/ToolBarUtils.java index 9cdf7330..20cbafe9 100644 --- a/src/main/java/com/wdf/fudoc/util/ToolBarUtils.java +++ b/src/main/java/com/wdf/fudoc/util/ToolBarUtils.java @@ -33,7 +33,8 @@ public static JComponent addActionToToolBar(JComponent targetComponent, String p ActionToolbarImpl toolbar = (ActionToolbarImpl) ActionManager.getInstance().createActionToolbar(place, actionGroup, true); toolbar.setTargetComponent(targetComponent); toolbar.setForceMinimumSize(true); - toolbar.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); + // Note: setLayoutPolicy() is deprecated and removed in IDEA 2025.1+ + // The default behavior is already NOWRAP, so this call is not needed Utils.setSmallerFontForChildren(toolbar); toolbar.getComponent().setBackground(targetComponent.getBackground()); return toolbar.getComponent(); @@ -53,7 +54,8 @@ public static JPanel genToolBarPanel(String place, ActionGroup actionGroup, Stri ActionToolbarImpl toolbar = (ActionToolbarImpl) ActionManager.getInstance().createActionToolbar(place, actionGroup, true); toolbar.setTargetComponent(toolBarPanel); toolbar.setForceMinimumSize(true); - toolbar.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); + // Note: setLayoutPolicy() is deprecated and removed in IDEA 2025.1+ + // The default behavior is already NOWRAP, so this call is not needed Utils.setSmallerFontForChildren(toolbar); toolBarPanel.add(toolbar.getComponent(), layout); return toolBarPanel; diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 3231719a..c756768d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,14 +1,14 @@ com.wdf.api FuDoc - 222.1.8.9 + 251.1.8.9 WANG DINGFU - + JavaScript @@ -16,6 +16,7 @@ com.intellij.modules.java com.jetbrains.restClient org.jetbrains.idea.maven + com.intellij.modules.json @@ -23,8 +24,8 @@ - - + + diff --git a/src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt b/src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt.bak similarity index 89% rename from src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt rename to src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt.bak index be61da00..07817005 100644 --- a/src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt +++ b/src/test/kotlin/com/github/wangdingfu/fuapidocplugin/MyPluginTest.kt.bak @@ -5,13 +5,14 @@ import com.intellij.psi.xml.XmlFile import com.intellij.testFramework.TestDataPath import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.PsiErrorElementUtil +import org.junit.jupiter.api.Assertions.* @TestDataPath("\$CONTENT_ROOT/src/test/testData") class MyPluginTest : BasePlatformTestCase() { fun testXMLFile() { val psiFile = myFixture.configureByText(XmlFileType.INSTANCE, "bar") - val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java) + val xmlFile = assertInstanceOf(XmlFile::class.java, psiFile) assertFalse(PsiErrorElementUtil.hasErrors(project, xmlFile.virtualFile)) From 0cdec2ff357149f2572c7866cfb72d8550f75166 Mon Sep 17 00:00:00 2001 From: lijianguo Date: Wed, 12 Nov 2025 13:32:49 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E9=80=82=E9=85=8D2025.1.7=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20-fix1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.properties | 4 +- .../fudoc/apidoc/sync/SyncFuDocExecutor.java | 26 ++++-- .../fudoc/components/FuEditorComponent.java | 4 +- .../futool/beancopy/FuBeanCopyCompletion.java | 32 +++++-- .../wdf/fudoc/request/SendRequestHandler.java | 88 ++++++++++++++++--- .../fudoc/request/execute/HttpExecutor.java | 12 ++- .../request/http/helper/FuRequestFactory.java | 3 +- .../fudoc/request/manager/FuCurlManager.java | 6 +- .../manager/FuRequestToolBarManager.java | 19 ++-- .../request/tab/request/RequestTabView.java | 11 +++ .../request/tab/request/ResponseTabView.java | 48 +++++++++- .../fudoc/request/view/HttpDialogView.java | 44 +++++++++- .../view/toolwindow/FuRequestWindow.java | 44 ++++++++-- .../wdf/fudoc/storage/FuRequestStorage.java | 3 +- .../com/wdf/fudoc/test/action/TestAction.java | 6 +- .../action/editor/FuEditorFormatAction.java | 8 +- .../com/wdf/fudoc/util/FuEditorSettings.java | 4 + 17 files changed, 303 insertions(+), 59 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb879..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/wdf/fudoc/apidoc/sync/SyncFuDocExecutor.java b/src/main/java/com/wdf/fudoc/apidoc/sync/SyncFuDocExecutor.java index 0dc23db1..3d5ab185 100644 --- a/src/main/java/com/wdf/fudoc/apidoc/sync/SyncFuDocExecutor.java +++ b/src/main/java/com/wdf/fudoc/apidoc/sync/SyncFuDocExecutor.java @@ -23,13 +23,27 @@ @Slf4j public class SyncFuDocExecutor { - + /** + * 延迟初始化的服务缓存 Map + * IDEA 2025.1+ 不允许在静态初始化块中获取服务实例,改为按需获取 + */ private static final Map syncFuDocMap = new ConcurrentHashMap<>(); - static { - syncFuDocMap.put(ApiDocSystem.YAPI, ServiceHelper.getService(SyncToYApiStrategy.class)); - syncFuDocMap.put(ApiDocSystem.SHOW_DOC, ServiceHelper.getService(SyncShowDocStrategy.class)); - syncFuDocMap.put(ApiDocSystem.API_FOX, ServiceHelper.getService(SyncToApiFoxStrategy.class)); + /** + * 获取同步策略服务实例(延迟加载) + * + * @param apiDocSystem API 文档系统类型 + * @return 对应的同步策略实例 + */ + private static SyncFuDocStrategy getSyncStrategy(ApiDocSystem apiDocSystem) { + return syncFuDocMap.computeIfAbsent(apiDocSystem, system -> { + return switch (system) { + case YAPI -> ServiceHelper.getService(SyncToYApiStrategy.class); + case SHOW_DOC -> ServiceHelper.getService(SyncShowDocStrategy.class); + case API_FOX -> ServiceHelper.getService(SyncToApiFoxStrategy.class); + default -> null; + }; + }); } @@ -38,7 +52,7 @@ public static void sync(ApiDocSystem apiDocSystem, BaseSyncConfigData baseSyncCo return; } try { - SyncFuDocStrategy syncFuDocStrategy = syncFuDocMap.get(apiDocSystem); + SyncFuDocStrategy syncFuDocStrategy = getSyncStrategy(apiDocSystem); if (Objects.isNull(syncFuDocStrategy)) { return; } diff --git a/src/main/java/com/wdf/fudoc/components/FuEditorComponent.java b/src/main/java/com/wdf/fudoc/components/FuEditorComponent.java index 844493d3..4fd8b45b 100644 --- a/src/main/java/com/wdf/fudoc/components/FuEditorComponent.java +++ b/src/main/java/com/wdf/fudoc/components/FuEditorComponent.java @@ -229,7 +229,9 @@ private synchronized void refreshUI() { WriteCommandAction.runWriteCommandAction(currProject, () -> this.editor.getDocument().setText("")); this.editor.setHighlighter(highlighterFactory.createEditorHighlighter(currProject, lightVirtualFile)); } else { - this.content = this.content.replaceAll("\r", ""); + // IDEA 2025.1+ 修复: 不处理换行符,保持 JSON 内容原样 + // JSON 格式化已经在外部处理,这里只负责显示 + // 软换行由编辑器设置控制,不需要修改内容本身 this.editor.setViewer(false); // 重置文本内容 WriteCommandAction.runWriteCommandAction(currProject, () -> this.editor.getDocument().setText(this.content)); diff --git a/src/main/java/com/wdf/fudoc/futool/beancopy/FuBeanCopyCompletion.java b/src/main/java/com/wdf/fudoc/futool/beancopy/FuBeanCopyCompletion.java index f1969749..1f0d58d7 100644 --- a/src/main/java/com/wdf/fudoc/futool/beancopy/FuBeanCopyCompletion.java +++ b/src/main/java/com/wdf/fudoc/futool/beancopy/FuBeanCopyCompletion.java @@ -61,7 +61,16 @@ public class FuBeanCopyCompletion extends CompletionContributor { @Override public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) { PsiElement position = parameters.getPosition(); - PsiReference reference = position.getParent().getFirstChild().getReference(); + // IDEA 2025.1+ API 变更: 增加 null 检查,防止 NPE + PsiElement parent = position.getParent(); + if (Objects.isNull(parent)) { + return; + } + PsiElement firstChild = parent.getFirstChild(); + if (Objects.isNull(firstChild)) { + return; + } + PsiReference reference = firstChild.getReference(); if (Objects.isNull(reference)) { return; } @@ -230,14 +239,21 @@ private static PsiClass findPsiClass(PsiElement psiElement) { for (PsiElement child : children) { if (child instanceof PsiTypeElement) { PsiTypeElement psiTypeElement = (PsiTypeElement) child; - PsiElement firstChild = psiTypeElement.getFirstChild(); - PsiReference reference = firstChild.getReference(); - if (Objects.isNull(reference)) { - continue; + // IDEA 2025.1+ API 变更: 优先使用 PsiTypeElement.getType() 方法获取类型 + PsiType type = psiTypeElement.getType(); + if (type instanceof PsiClassType) { + return ((PsiClassType) type).resolve(); } - PsiElement resolve = reference.resolve(); - if (resolve instanceof PsiClass) { - return (PsiClass) resolve; + // 回退方案: 通过 firstChild 获取引用 + PsiElement firstChild = psiTypeElement.getFirstChild(); + if (Objects.nonNull(firstChild)) { + PsiReference reference = firstChild.getReference(); + if (Objects.nonNull(reference)) { + PsiElement resolve = reference.resolve(); + if (resolve instanceof PsiClass) { + return (PsiClass) resolve; + } + } } } } diff --git a/src/main/java/com/wdf/fudoc/request/SendRequestHandler.java b/src/main/java/com/wdf/fudoc/request/SendRequestHandler.java index 2111cb9c..a71e16b9 100644 --- a/src/main/java/com/wdf/fudoc/request/SendRequestHandler.java +++ b/src/main/java/com/wdf/fudoc/request/SendRequestHandler.java @@ -27,6 +27,8 @@ public class SendRequestHandler { private final HttpCallback httpCallback; private Future sendHttpTask; private final AtomicBoolean sendStatus = new AtomicBoolean(false); + // IDEA 2025.1+ 新增: 防止 doSendAfter 被重复调用 + private final AtomicBoolean afterCalled = new AtomicBoolean(false); private final FuLogger fuLogger; @@ -47,20 +49,59 @@ public void doSend(FuHttpRequestData httpRequestData) { project.getMessageBus().syncPublisher(FuDocActionListener.TOPIC).action(FuDocAction.FU_REQUEST.getCode()); //清空日志 fuLogger.clear(); - this.sendHttpTask = ThreadUtil.execAsync(() -> { - sendStatus.set(true); + + // IDEA 2025.1+ 修复: 先设置状态,再调用 doSendBefore + sendStatus.set(true); + afterCalled.set(false); + + // IDEA 2025.1+ 修复: 在 EDT 线程调用 doSendBefore,确保 UI 更新正确 + ApplicationManager.getApplication().invokeLater(() -> { httpCallback.doSendBefore(httpRequestData); - //发起http请求执行 - HttpApiExecutor.doSendRequest(project, httpRequestData, fuLogger); - if (Objects.isNull(this.sendHttpTask) || this.sendHttpTask.isCancelled()) { - return; + }, ModalityState.any()); + + this.sendHttpTask = ThreadUtil.execAsync(() -> { + try { + //发起http请求执行 + HttpApiExecutor.doSendRequest(project, httpRequestData, fuLogger); + log.info("HTTP请求执行完成"); + } catch (Exception e) { + log.error("发送HTTP请求异常", e); + } finally { + log.info("进入 finally 块, sendHttpTask={}, isCancelled={}", + this.sendHttpTask, + this.sendHttpTask != null ? this.sendHttpTask.isCancelled() : "null"); + + // IDEA 2025.1+ 修复: 无论成功失败,总是调用 doSendAfter 恢复 UI 状态 + // 使用 CAS 确保只调用一次 + if (afterCalled.compareAndSet(false, true)) { + ApplicationManager.getApplication().invokeLater(() -> { + log.info("EDT 线程执行 doSendAfter (from finally)"); + try { + // 只有未被取消的请求才填充响应数据 + if (Objects.nonNull(this.sendHttpTask) && !this.sendHttpTask.isCancelled()) { + log.info("调用 doSendAfter with data"); + httpCallback.doSendAfter(httpRequestData); + } else { + // 被取消的请求也要恢复 UI,但不填充数据 + log.info("调用 doSendAfter with null"); + httpCallback.doSendAfter(null); + } + } catch (Exception e) { + log.error("doSendAfter 执行异常", e); + } finally { + //执行后置逻辑 + log.info("设置 sendStatus=false, sendHttpTask=null"); + sendStatus.set(false); + this.sendHttpTask = null; + } + }, ModalityState.any()); + } else { + log.info("doSendAfter 已被调用,跳过 (from finally)"); + // 即使 doSendAfter 被跳过,也要清理状态 + sendStatus.set(false); + this.sendHttpTask = null; + } } - ApplicationManager.getApplication().invokeLater(() -> { - httpCallback.doSendAfter(httpRequestData); - //执行后置逻辑 - sendStatus.set(false); - this.sendHttpTask = null; - }, ModalityState.any()); }); } @@ -74,9 +115,28 @@ public void stopHttp() { return; } try { + // IDEA 2025.1+ 修复: 取消任务 + log.info("用户点击 Stop 按钮,取消 HTTP 请求"); this.sendHttpTask.cancel(true); - //执行后置逻辑 - sendStatus.set(false); + + // IDEA 2025.1+ 修复: 立即在 EDT 线程恢复 UI,不等待 finally 块 + // 因为被取消的任务可能卡在阻塞 I/O 中,finally 块可能不会立即执行 + // 使用 CAS 确保只调用一次 + if (afterCalled.compareAndSet(false, true)) { + ApplicationManager.getApplication().invokeLater(() -> { + log.info("Stop 按钮触发: 立即调用 doSendAfter 恢复 UI"); + try { + httpCallback.doSendAfter(null); + } catch (Exception e) { + log.error("doSendAfter 执行异常", e); + } finally { + sendStatus.set(false); + this.sendHttpTask = null; + } + }, ModalityState.any()); + } else { + log.info("doSendAfter 已被调用,跳过 (from stopHttp)"); + } } catch (Exception e) { log.info("终止http请求", e); } diff --git a/src/main/java/com/wdf/fudoc/request/execute/HttpExecutor.java b/src/main/java/com/wdf/fudoc/request/execute/HttpExecutor.java index 4887a0c0..028e4d67 100644 --- a/src/main/java/com/wdf/fudoc/request/execute/HttpExecutor.java +++ b/src/main/java/com/wdf/fudoc/request/execute/HttpExecutor.java @@ -65,11 +65,19 @@ public static void execute(FuHttpRequestData fuHttpRequestData, FuRequestConfigP } catch (Exception e) { FuResponseData fuResponseData = FuHttpResponseBuilder.ifNecessaryCreateResponse(fuHttpRequestData); log.info("请求接口【{}】异常", requestUrl, e); - if (e.getCause() instanceof ConnectException) { + + // IDEA 2025.1+ 修复: 安全处理异常信息,防止 NPE + Throwable cause = e.getCause(); + if (cause instanceof ConnectException) { fuResponseData.setErrorDetail("错误:connect ECONNREFUSED " + httpRequest.getConnection().getUrl().getAuthority()); fuResponseData.setResponseType(ResponseType.ERR_CONNECTION_REFUSED); } else { - fuResponseData.setErrorDetail(e.getCause().getMessage()); + // 优先使用 cause 的消息,如果没有 cause 则使用异常本身的消息 + String errorMessage = cause != null ? cause.getMessage() : e.getMessage(); + if (errorMessage == null) { + errorMessage = e.getClass().getSimpleName(); + } + fuResponseData.setErrorDetail(errorMessage); fuResponseData.setResponseType(ResponseType.ERR_UNKNOWN); } //记录日志到Console中展示 diff --git a/src/main/java/com/wdf/fudoc/request/http/helper/FuRequestFactory.java b/src/main/java/com/wdf/fudoc/request/http/helper/FuRequestFactory.java index 86987a86..e2c61ab2 100644 --- a/src/main/java/com/wdf/fudoc/request/http/helper/FuRequestFactory.java +++ b/src/main/java/com/wdf/fudoc/request/http/helper/FuRequestFactory.java @@ -51,7 +51,8 @@ public static FuRequest create(AnActionEvent e, HttpRequestPsiFile httpRequestPs Project project = httpRequest.getProject(); //获取psiClass和psiMethod HttpRequestTarget requestTarget = httpRequest.getRequestTarget(); - HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(project, null); + // IDEA 2025.1+ API 变更: getDefault() 的 contextFile 参数不再允许为 null + HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(project, httpRequestPsiFile); if (Objects.isNull(requestTarget)) { return null; } diff --git a/src/main/java/com/wdf/fudoc/request/manager/FuCurlManager.java b/src/main/java/com/wdf/fudoc/request/manager/FuCurlManager.java index b1bd87a3..31195ac6 100644 --- a/src/main/java/com/wdf/fudoc/request/manager/FuCurlManager.java +++ b/src/main/java/com/wdf/fudoc/request/manager/FuCurlManager.java @@ -38,7 +38,11 @@ public static String toCurl(Project project, FuHttpRequestData requestData) { return FuStringUtils.EMPTY; } CurlRequestBuilder curlRequestBuilder = new CurlRequestBuilder(); - return (String) HttpRequestPsiConverter.convertFromHttpRequest(newRequest, HttpRequestVariableSubstitutor.getDefault(project, null), (RequestBuilder) curlRequestBuilder); + // IDEA 2025.1+ API 变更: getDefault() 的 contextFile 参数不再允许为 null,使用 psiFile + HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(project, psiFile); + Object curlResult = HttpRequestPsiConverter.convertFromHttpRequest(newRequest, substitutor, (RequestBuilder) curlRequestBuilder); + // IDEA 2025.1+ API 变更: convertFromHttpRequest 返回 Object,需要调用 toString() + return curlResult.toString(); } catch (Exception e) { log.error("生成curl命令失败", e); throw new FuDocException("生成curl命令失败"); diff --git a/src/main/java/com/wdf/fudoc/request/manager/FuRequestToolBarManager.java b/src/main/java/com/wdf/fudoc/request/manager/FuRequestToolBarManager.java index 45279b80..5c81b4ca 100644 --- a/src/main/java/com/wdf/fudoc/request/manager/FuRequestToolBarManager.java +++ b/src/main/java/com/wdf/fudoc/request/manager/FuRequestToolBarManager.java @@ -188,8 +188,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { addConfigServerPortAction(actionGroup); //新增提交issue mode addIssueAction(actionGroup); - //设置controller中是否展示左侧图标 - addControllerIconAction(actionGroup); + // IDEA 2025.1+ 修改: Controller图标开关已移到主工具栏,不再放在弹出菜单中 int x = 0, y = 0; InputEvent inputEvent = e.getInputEvent(); if (inputEvent instanceof MouseEvent mouseEvent) { @@ -207,7 +206,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { defaultActionGroup.addSeparator(); - //添加帮助文档按钮 + //添加添加帮助文档按钮 defaultActionGroup.add(new AnAction("Help", "Help", FuDocIcons.FU_DOC) { @Override public void actionPerformed(@NotNull AnActionEvent e) { @@ -215,12 +214,13 @@ public void actionPerformed(@NotNull AnActionEvent e) { } }); + // IDEA 2025.1+ 新增: Controller左侧图标开关按钮(从弹出菜单移到主工具栏) + defaultActionGroup.add(new ToggleAction("Controller左侧图标", "显示/隐藏Controller左侧的图标", AllIcons.Gutter.Colors) { + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } - } - - private void addControllerIconAction(DefaultActionGroup defaultActionGroup){ - //添加同步接口文档事件 - defaultActionGroup.add(new ToggleAction("Controller左侧图标") { @Override public boolean isSelected(@NotNull AnActionEvent e) { Project project = e.getProject(); @@ -238,9 +238,10 @@ public void setSelected(@NotNull AnActionEvent e, boolean state) { instance.saveData(fuDocConfigPO); } }); - } + } + private void addIssueAction(DefaultActionGroup defaultActionGroup) { DefaultActionGroup issueActionGroup = DefaultActionGroup.createPopupGroup(() -> "提交Issue"); for (IssueSource value : IssueSource.values()) { diff --git a/src/main/java/com/wdf/fudoc/request/tab/request/RequestTabView.java b/src/main/java/com/wdf/fudoc/request/tab/request/RequestTabView.java index eeb4297f..98678284 100644 --- a/src/main/java/com/wdf/fudoc/request/tab/request/RequestTabView.java +++ b/src/main/java/com/wdf/fudoc/request/tab/request/RequestTabView.java @@ -195,6 +195,12 @@ public void initData(FuHttpRequestData httpRequestData) { @Override public void doSendBefore(FuHttpRequestData fuHttpRequestData) { + // IDEA 2025.1+ 新增: 禁用 Send 按钮,防止重复发送 + System.out.println("=== RequestTabView.doSendBefore: 禁用 Send 按钮 ==="); + logger.info("RequestTabView.doSendBefore: 禁用 Send 按钮"); + sendBtn.setEnabled(false); + sendBtn.setText("Sending..."); + //设置请求类型 setRequestType(requestTypeComponent.getSelectedItem() + FuStringUtils.EMPTY); httpHeaderTab.doSendBefore(fuHttpRequestData); @@ -205,6 +211,11 @@ public void doSendBefore(FuHttpRequestData fuHttpRequestData) { @Override public void doSendAfter(FuHttpRequestData fuHttpRequestData) { + // IDEA 2025.1+ 新增: 启用 Send 按钮(无论请求成功、失败还是被取消) + System.out.println("=== RequestTabView.doSendAfter: 启用 Send 按钮, data=" + (fuHttpRequestData != null) + " ==="); + logger.info("RequestTabView.doSendAfter: 启用 Send 按钮, data=" + (fuHttpRequestData != null)); + sendBtn.setEnabled(true); + sendBtn.setText("Send"); } /** diff --git a/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java b/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java index ed6b643e..6aa89e65 100644 --- a/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java +++ b/src/main/java/com/wdf/fudoc/request/tab/request/ResponseTabView.java @@ -3,6 +3,8 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.http.HttpResponse; import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.wdf.fudoc.compat.JsonFileTypeCompat; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; @@ -103,7 +105,10 @@ public void initData(FuHttpRequestData httpRequestData) { initRootPane(); } else { //请求成功 渲染响应数据到编辑器中 - fuEditorComponent.setContent(JSONUtil.formatJsonStr(response.getContent())); + String content = response.getContent(); + // IDEA 2025.1+ 修复: 改进 JSON 格式化处理,避免长字符串换行导致双引号丢失 + String formattedContent = formatJsonContent(content); + fuEditorComponent.setContent(formattedContent); switchPanel(1, fuEditorComponent.getMainPanel()); } } @@ -117,7 +122,12 @@ public void initData(FuHttpRequestData httpRequestData) { @Override public void doSendBefore(FuHttpRequestData fuHttpRequestData) { - //do nothing + // 请求发送前预留钩子 + } + + @Override + public void doSendAfter(FuHttpRequestData fuHttpRequestData) { + // 请求完成后,initData 会被调用来显示响应结果 } @Override @@ -155,4 +165,38 @@ public void initRootPane() { } } + /** + * 格式化 JSON 内容 + * 避免长字符串换行时导致格式问题 + * + * @param content 原始内容 + * @return 格式化后的内容 + */ + private String formatJsonContent(String content) { + if (FuStringUtils.isBlank(content)) { + return content; + } + try { + // IDEA 2025.1+ 修复: 使用 Jackson 进行 JSON 格式化 + // Jackson 会正确处理字符串中的转义字符,不会将 \r\n 展开为真实换行 + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + + // 先解析 JSON,再格式化输出 + Object json = mapper.readValue(content, Object.class); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json); + } catch (Exception e) { + // 如果 Jackson 格式化失败,尝试使用 Hutool + try { + if (JSONUtil.isTypeJSON(content)) { + return JSONUtil.formatJsonStr(content); + } + } catch (Exception ex) { + // 忽略 + } + // 都失败则返回原始内容 + return content; + } + } + } diff --git a/src/main/java/com/wdf/fudoc/request/view/HttpDialogView.java b/src/main/java/com/wdf/fudoc/request/view/HttpDialogView.java index b53e9c2f..f170da3b 100644 --- a/src/main/java/com/wdf/fudoc/request/view/HttpDialogView.java +++ b/src/main/java/com/wdf/fudoc/request/view/HttpDialogView.java @@ -9,6 +9,8 @@ import com.wdf.fudoc.components.factory.FuTabBuilder; import com.wdf.fudoc.components.listener.SendHttpListener; import com.wdf.fudoc.components.message.MessageComponent; +import cn.fudoc.common.msg.FuMsgBuilder; +import cn.fudoc.common.enumtype.FuColor; import com.wdf.fudoc.request.HttpCallback; import com.wdf.fudoc.request.SendRequestHandler; import com.wdf.fudoc.request.callback.FuRequestCallback; @@ -33,6 +35,8 @@ import javax.swing.*; import javax.swing.border.Border; import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Objects; /** @@ -233,11 +237,43 @@ public void doSendBefore(FuHttpRequestData fuHttpRequestData) { @Override public void doSendAfter(FuHttpRequestData fuHttpRequestData) { - this.fuTabBuilder.select(ResponseTabView.RESPONSE); + // IDEA 2025.1+ 修复: 必须调用 requestTabView.doSendAfter 来恢复 Send 按钮状态 + // 无论请求成功、失败还是被取消,都必须调用以恢复 UI 状态 this.requestTabView.doSendAfter(fuHttpRequestData); - this.responseTabView.initData(fuHttpRequestData); - //切换消息展示 - messageComponent.switchInfo(); + + // IDEA 2025.1+ 修复: 被取消的请求(fuHttpRequestData == null)不填充响应数据 + if (fuHttpRequestData != null) { + this.fuTabBuilder.select(ResponseTabView.RESPONSE); + this.responseTabView.initData(fuHttpRequestData); + + // IDEA 2025.1+ 新增: 在底部状态栏显示请求结果 + Integer httpCode = fuHttpRequestData.getHttpCode(); + Long time = fuHttpRequestData.getTime(); + if (httpCode != null && time != null) { + boolean isOk = fuHttpRequestData.isOk(); + String statusIcon = isOk ? "✓" : "✗"; + FuColor color = isOk ? FuColor.GREEN : FuColor.RED; + + // 格式化请求时间(精确到毫秒) + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); + String requestTime = sdf.format(new Date()); + + cn.fudoc.common.msg.bo.FuMsgBO msgBO = FuMsgBuilder.getInstance() + .text(statusIcon + " 请求完成 | 状态: ") + .text(String.valueOf(httpCode), color) + .text(" | 耗时: ") + .text(time + "ms", FuColor.GREEN) + .text(" | 时间: " + requestTime) + .build(); + messageComponent.setMsg(msgBO); + } else { + //切换消息展示 + messageComponent.switchInfo(); + } + } else { + //切换消息展示 + messageComponent.switchInfo(); + } } diff --git a/src/main/java/com/wdf/fudoc/request/view/toolwindow/FuRequestWindow.java b/src/main/java/com/wdf/fudoc/request/view/toolwindow/FuRequestWindow.java index cad733e4..f4c3d12d 100644 --- a/src/main/java/com/wdf/fudoc/request/view/toolwindow/FuRequestWindow.java +++ b/src/main/java/com/wdf/fudoc/request/view/toolwindow/FuRequestWindow.java @@ -11,6 +11,8 @@ import com.wdf.fudoc.components.factory.FuTabBuilder; import com.wdf.fudoc.components.listener.SendHttpListener; import com.wdf.fudoc.components.message.MessageComponent; +import cn.fudoc.common.msg.FuMsgBuilder; +import cn.fudoc.common.enumtype.FuColor; import com.wdf.fudoc.request.HttpCallback; import com.wdf.fudoc.request.SendRequestHandler; import com.wdf.fudoc.request.callback.FuRequestCallback; @@ -32,6 +34,8 @@ import javax.swing.*; import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; /** * @author wangdingfu @@ -90,7 +94,8 @@ public boolean getSendStatus() { } public FuRequestWindow(@NotNull Project project, ToolWindow toolWindow) { - super(Boolean.TRUE, Boolean.TRUE); + // IDEA 2025.1+ 修复: 使用 primitive boolean 而非 Boolean 对象 + super(true, true); this.project = project; this.toolWindow = toolWindow; this.rootPanel = new JPanel(new BorderLayout()); @@ -157,10 +162,39 @@ public void doSendBefore(FuHttpRequestData fuHttpRequestData) { @Override public void doSendAfter(FuHttpRequestData fuHttpRequestData) { ApplicationManager.getApplication().invokeLater(() -> { - //填充响应面板数据 - this.responseTabView.initData(fuHttpRequestData); - //填充响应头面板数据 - this.responseHeaderTabView.initData(fuHttpRequestData); + // IDEA 2025.1+ 修复: 必须调用 requestTabView.doSendAfter 来恢复 Send 按钮状态 + // 无论请求成功、失败还是被取消,都必须调用以恢复 UI 状态 + this.requestTabView.doSendAfter(fuHttpRequestData); + + // IDEA 2025.1+ 修复: 被取消的请求(fuHttpRequestData == null)不填充响应数据 + if (fuHttpRequestData != null) { + //填充响应面板数据 + this.responseTabView.initData(fuHttpRequestData); + //填充响应头面板数据 + this.responseHeaderTabView.initData(fuHttpRequestData); + + // IDEA 2025.1+ 新增: 在底部状态栏显示请求结果 + Integer httpCode = fuHttpRequestData.getHttpCode(); + Long time = fuHttpRequestData.getTime(); + if (httpCode != null && time != null) { + boolean isOk = fuHttpRequestData.isOk(); + String statusIcon = isOk ? "✓" : "✗"; + FuColor color = isOk ? FuColor.GREEN : FuColor.RED; + + // 格式化请求时间(精确到毫秒) + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); + String requestTime = sdf.format(new Date()); + + cn.fudoc.common.msg.bo.FuMsgBO msgBO = FuMsgBuilder.getInstance() + .text(statusIcon + " 请求完成 | 状态: ") + .text(String.valueOf(httpCode), color) + .text(" | 耗时: ") + .text(time + "ms", FuColor.GREEN) + .text(" | 时间: " + requestTime) + .build(); + this.messageComponent.setMsg(msgBO); + } + } }); } diff --git a/src/main/java/com/wdf/fudoc/storage/FuRequestStorage.java b/src/main/java/com/wdf/fudoc/storage/FuRequestStorage.java index 52679e4f..a90018a5 100644 --- a/src/main/java/com/wdf/fudoc/storage/FuRequestStorage.java +++ b/src/main/java/com/wdf/fudoc/storage/FuRequestStorage.java @@ -53,7 +53,8 @@ public static FuHttpClient read(Project project, PsiClass psiClass, PsiMethod ps if (Objects.isNull(httpRequestPsiFile)) { return null; } - HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(project, null); + // IDEA 2025.1+ API 变更: getDefault() 的 contextFile 参数不再允许为 null + HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(project, httpRequestPsiFile); HttpRequestBlock[] requestBlocks = HttpRequestPsiUtils.getRequestBlocks(httpRequestPsiFile); for (HttpRequestBlock requestBlock : requestBlocks) { HttpRequest request = requestBlock.getRequest(); diff --git a/src/main/java/com/wdf/fudoc/test/action/TestAction.java b/src/main/java/com/wdf/fudoc/test/action/TestAction.java index b85fa642..69efccb0 100644 --- a/src/main/java/com/wdf/fudoc/test/action/TestAction.java +++ b/src/main/java/com/wdf/fudoc/test/action/TestAction.java @@ -56,11 +56,12 @@ private void apiTest(AnActionEvent e) { } private void curlTest(AnActionEvent e) { - HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(e.getProject(), null); //读取http文件 PsiFile psiFile = e.getData(LangDataKeys.PSI_FILE); HttpRequest httpRequest = FuRequestUtils.getHttpRequest((HttpRequestPsiFile) psiFile, e.getData(LangDataKeys.EDITOR)); + // IDEA 2025.1+ API 变更: getDefault() 的 contextFile 参数不再允许为 null + HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(e.getProject(), (HttpRequestPsiFile) psiFile); try { Object o = HttpRequestPsiConverter.convertFromHttpRequest(httpRequest, substitutor, (RequestBuilder) (new CurlRequestBuilder())); System.out.println(o); @@ -71,11 +72,12 @@ private void curlTest(AnActionEvent e) { private void request(AnActionEvent e) { - HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(e.getProject(), null); //读取http文件 PsiFile psiFile = e.getData(LangDataKeys.PSI_FILE); HttpRequest httpRequest = FuRequestUtils.getHttpRequest((HttpRequestPsiFile) psiFile, e.getData(LangDataKeys.EDITOR)); + // IDEA 2025.1+ API 变更: getDefault() 的 contextFile 参数不再允许为 null + HttpRequestVariableSubstitutor substitutor = HttpRequestVariableSubstitutor.getDefault(e.getProject(), (HttpRequestPsiFile) psiFile); try { Object o = HttpRequestPsiConverter.convertFromHttpRequest(httpRequest, substitutor, (RequestBuilder) (new CurlRequestBuilder())); } catch (HttpRequestValidationException e3) { diff --git a/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java b/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java index 2d3bbb83..81bd5e80 100644 --- a/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java +++ b/src/main/java/com/wdf/fudoc/test/action/editor/FuEditorFormatAction.java @@ -37,8 +37,12 @@ public void update(@NotNull AnActionEvent e) { Editor editor = e.getData(CommonDataKeys.EDITOR); if (editor != null && e.getProject() != null) { PsiFile file = PsiDocumentManager.getInstance(e.getProject()).getPsiFile(editor.getDocument()); - if (Objects.nonNull(file) && FuStringUtils.isNotBlank(file.getName())) { - e.getPresentation().setEnabledAndVisible(file.getName().equals(FuDocConstants.FU_DOC_FILE + "json")); + if (Objects.nonNull(file)) { + // IDEA 2025.1+ 修复: 使用 JsonFileTypeCompat 判断文件类型,而不是硬编码文件名 + boolean isJsonFile = JsonFileTypeCompat.isJsonFileType(file.getFileType()); + e.getPresentation().setEnabledAndVisible(isJsonFile); + } else { + e.getPresentation().setEnabledAndVisible(false); } } else { e.getPresentation().setEnabledAndVisible(false); diff --git a/src/main/java/com/wdf/fudoc/util/FuEditorSettings.java b/src/main/java/com/wdf/fudoc/util/FuEditorSettings.java index 18f706d8..768a3093 100644 --- a/src/main/java/com/wdf/fudoc/util/FuEditorSettings.java +++ b/src/main/java/com/wdf/fudoc/util/FuEditorSettings.java @@ -33,5 +33,9 @@ public static void defaultSetting(Editor editor) { editorSettings.setAdditionalLinesCount(0); // 不显示换行符号 editorSettings.setCaretRowShown(false); + // IDEA 2025.1+ 修复: 启用软换行,避免长 JSON 字符串显示异常 + editorSettings.setUseSoftWraps(true); + // 确保在单词边界换行,而不是在字符串中间 + editorSettings.setUseCustomSoftWrapIndent(false); } } From a5c34dba83675620fd78fd1795b266316e36e799 Mon Sep 17 00:00:00 2001 From: lijianguo Date: Wed, 12 Nov 2025 15:06:08 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E9=80=82=E9=85=8D2025.1.7=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20-feature=20api=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wdf/fudoc/apilist/constant/GroupType.java | 36 + .../wdf/fudoc/apilist/pojo/ApiListGroup.java | 63 ++ .../wdf/fudoc/apilist/pojo/ApiListItem.java | 111 +++ .../apilist/service/ApiListCollector.java | 170 +++++ .../apilist/strategy/ApiGroupStrategy.java | 23 + .../apilist/strategy/ModuleGroupStrategy.java | 74 ++ .../apilist/strategy/PrefixGroupStrategy.java | 50 ++ .../fudoc/apilist/tree/ApiItemTreeNode.java | 33 + .../apilist/tree/ApiListTreeCellRenderer.java | 175 +++++ .../wdf/fudoc/apilist/tree/ApiTreeNode.java | 39 ++ .../apilist/tree/ControllerTreeNode.java | 41 ++ .../wdf/fudoc/apilist/tree/GroupTreeNode.java | 26 + .../fudoc/apilist/tree/ModuleTreeNode.java | 25 + .../fudoc/apilist/tree/PackageTreeNode.java | 25 + .../fudoc/apilist/view/ApiListToolWindow.java | 653 ++++++++++++++++++ .../view/ApiListToolWindowFactory.java | 29 + src/main/resources/META-INF/plugin.xml | 8 + 17 files changed, 1581 insertions(+) create mode 100644 src/main/java/com/wdf/fudoc/apilist/constant/GroupType.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/pojo/ApiListGroup.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/pojo/ApiListItem.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/service/ApiListCollector.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/strategy/ApiGroupStrategy.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/strategy/ModuleGroupStrategy.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/strategy/PrefixGroupStrategy.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/ApiItemTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/ApiListTreeCellRenderer.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/ApiTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/ControllerTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/GroupTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/ModuleTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/tree/PackageTreeNode.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindow.java create mode 100644 src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindowFactory.java diff --git a/src/main/java/com/wdf/fudoc/apilist/constant/GroupType.java b/src/main/java/com/wdf/fudoc/apilist/constant/GroupType.java new file mode 100644 index 00000000..6a7232c3 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/constant/GroupType.java @@ -0,0 +1,36 @@ +package com.wdf.fudoc.apilist.constant; + +import lombok.Getter; + +/** + * API 列表分组类型 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public enum GroupType { + + /** + * 按模块分组 + */ + MODULE("按 Module 分组", "module"), + + /** + * 按 URL 前缀分组 + */ + PREFIX("按 URL 前缀分组", "prefix"); + + private final String displayName; + private final String code; + + GroupType(String displayName, String code) { + this.displayName = displayName; + this.code = code; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListGroup.java b/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListGroup.java new file mode 100644 index 00000000..f9db8846 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListGroup.java @@ -0,0 +1,63 @@ +package com.wdf.fudoc.apilist.pojo; + +import com.wdf.fudoc.apilist.constant.GroupType; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * API 列表分组对象 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +@Setter +public class ApiListGroup { + + /** + * 分组名称 + */ + private final String groupName; + + /** + * 分组类型 + */ + private final GroupType groupType; + + /** + * 该分组下的 API 列表 + */ + private final List items; + + public ApiListGroup(@NotNull String groupName, @NotNull GroupType groupType) { + this.groupName = groupName; + this.groupType = groupType; + this.items = new ArrayList<>(); + } + + /** + * 添加 API 项 + */ + public void addItem(ApiListItem item) { + this.items.add(item); + } + + /** + * 获取该分组下的 API 数量 + */ + public int getItemCount() { + return items.size(); + } + + /** + * 获取显示文本 (分组名 + 数量) + */ + @NotNull + public String getDisplayText() { + return groupName + " (" + getItemCount() + ")"; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListItem.java b/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListItem.java new file mode 100644 index 00000000..01823147 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/pojo/ApiListItem.java @@ -0,0 +1,111 @@ +package com.wdf.fudoc.apilist.pojo; + +import com.intellij.psi.PsiMethod; +import com.wdf.fudoc.apidoc.constant.enumtype.RequestType; +import lombok.Getter; +import lombok.Setter; +import com.wdf.fudoc.util.FuStringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * API 列表项数据对象 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +@Setter +public class ApiListItem { + + /** + * 对应的 PsiMethod + */ + private final PsiMethod psiMethod; + + /** + * API 请求 URL + */ + private final String url; + + /** + * 请求类型 (GET/POST/PUT/DELETE etc.) + */ + private final RequestType requestType; + + /** + * API 标题/描述 (从注释中提取) + */ + private final String title; + + /** + * 所属 Module 名称 + */ + private final String moduleName; + + /** + * Controller 类的全限定名 + */ + private final String className; + + /** + * URL 路径前缀 (第一级路径, 如 /api, /user) + */ + private String urlPrefix; + + public ApiListItem(@NotNull PsiMethod psiMethod, + @NotNull String url, + @NotNull RequestType requestType, + @Nullable String title, + @Nullable String moduleName, + @NotNull String className) { + this.psiMethod = psiMethod; + this.url = url; + this.requestType = requestType; + this.title = title; + this.moduleName = moduleName; + this.className = className; + this.urlPrefix = extractUrlPrefix(url); + } + + /** + * 提取 URL 的第一级路径作为前缀 + * 例如: /api/user/list -> /api + * /user/info -> /user + * / -> / + */ + private String extractUrlPrefix(String url) { + if (FuStringUtils.isBlank(url) || "/".equals(url)) { + return "/"; + } + // 移除开头的 / + String path = url.startsWith("/") ? url.substring(1) : url; + // 找到第一个 / 的位置 + int firstSlash = path.indexOf('/'); + if (firstSlash > 0) { + return "/" + path.substring(0, firstSlash); + } + // 如果没有第二个 /, 说明只有一级路径 + return "/" + path; + } + + /** + * 获取显示文本 + */ + @NotNull + public String getDisplayText() { + if (FuStringUtils.isNotBlank(title)) { + return title; + } + return url; + } + + /** + * 获取方法签名 (类名.方法名) + */ + @NotNull + public String getMethodSignature() { + String simpleClassName = className.substring(className.lastIndexOf('.') + 1); + return simpleClassName + "." + psiMethod.getName() + "()"; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/service/ApiListCollector.java b/src/main/java/com/wdf/fudoc/apilist/service/ApiListCollector.java new file mode 100644 index 00000000..8b60b4c5 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/service/ApiListCollector.java @@ -0,0 +1,170 @@ +package com.wdf.fudoc.apilist.service; + +import com.google.common.collect.Lists; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.wdf.fudoc.apidoc.constant.enumtype.RequestType; +import com.wdf.fudoc.apidoc.helper.DocCommentParseHelper; +import com.wdf.fudoc.apidoc.pojo.data.ApiDocCommentData; +import com.wdf.fudoc.apilist.pojo.ApiListItem; +import com.wdf.fudoc.util.FuApiUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import com.wdf.fudoc.util.FuStringUtils; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * API 列表收集服务 + * 复用 FuApiNavigationExecutor 的逻辑,收集项目中所有的 API + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Slf4j +public class ApiListCollector { + + private final Project project; + + private ApiListCollector(Project project) { + this.project = project; + } + + public static ApiListCollector getInstance(Project project) { + return new ApiListCollector(project); + } + + /** + * 收集项目中所有的 API + * + * @return API 列表 + */ + public List collectAllApis() { + return ApplicationManager.getApplication().runReadAction((Computable>) () -> + collectApis(GlobalSearchScope.projectScope(project)) + ); + } + + /** + * 收集指定范围内的 API + */ + private List collectApis(GlobalSearchScope scope) { + long start = System.currentTimeMillis(); + + // 1. 查找所有的 Controller 类 + List allController = FuApiUtils.findAllController(project, scope); + if (CollectionUtils.isEmpty(allController)) { + log.info("未找到任何 Controller 类"); + return Lists.newArrayList(); + } + + log.info("找到 {} 个 Controller 类", allController.size()); + + // 2. 分批处理 Controller 类,避免单次处理过多 + List> partitionList = Lists.partition(allController, 50); + List>> futures = Lists.newArrayList(); + + partitionList.forEach(partition -> + futures.add(CompletableFuture.supplyAsync(() -> + ApplicationManager.getApplication().runReadAction((Computable>) () -> + readApiFromClasses(partition) + ) + )) + ); + + // 3. 等待所有任务完成并合并结果 + List apiList = futures.stream() + .flatMap(f -> f.join().stream()) + .collect(Collectors.toList()); + + log.info("收集到 {} 个 API,耗时 {}ms", apiList.size(), System.currentTimeMillis() - start); + return apiList; + } + + /** + * 从 Controller 类列表中读取 API + */ + private List readApiFromClasses(List classList) { + List apiList = Lists.newArrayList(); + + for (PsiClass psiClass : classList) { + try { + // 获取类级别的 URL 前缀 + List classUrlList = FuApiUtils.getClassUrl(psiClass); + + // 获取类所属的 Module + Module module = ModuleUtil.findModuleForPsiElement(psiClass); + String moduleName = module != null ? module.getName() : "Unknown"; + + // 获取类的全限定名 + String className = psiClass.getQualifiedName(); + if (className == null) { + className = psiClass.getName(); + } + + // 遍历类中的所有方法 + for (PsiMethod psiMethod : psiClass.getMethods()) { + ApiListItem apiItem = readApiFromMethod(psiMethod, classUrlList, moduleName, className); + if (apiItem != null) { + apiList.add(apiItem); + } + } + } catch (Exception e) { + log.warn("读取 Controller 类 {} 的 API 时出错", psiClass.getName(), e); + } + } + + return apiList; + } + + /** + * 从方法中读取 API 信息 + */ + private ApiListItem readApiFromMethod(PsiMethod psiMethod, + List classUrlList, + String moduleName, + String className) { + try { + // 获取方法级别的 URL 和请求类型 + List methodUrlList = Lists.newArrayList(); + RequestType requestType = FuApiUtils.getMethodUrl(psiMethod, methodUrlList); + + // 如果没有找到请求类型或 URL,跳过 + if (Objects.isNull(requestType) || CollectionUtils.isEmpty(methodUrlList)) { + return null; + } + + // 拼接完整的 URL + List fullUrlList = FuApiUtils.joinUrl(classUrlList, methodUrlList); + if (CollectionUtils.isEmpty(fullUrlList)) { + return null; + } + + // 使用第一个 URL (通常只有一个) + String url = fullUrlList.get(0); + + // 解析方法注释,获取 API 标题 + ApiDocCommentData commentData = DocCommentParseHelper.parseComment(psiMethod); + String title = commentData.getCommentTitle(); + if (FuStringUtils.isBlank(title)) { + title = psiMethod.getName(); + } + + // 创建 API 列表项 + return new ApiListItem(psiMethod, url, requestType, title, moduleName, className); + + } catch (Exception e) { + log.warn("读取方法 {} 的 API 信息时出错", psiMethod.getName(), e); + return null; + } + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/strategy/ApiGroupStrategy.java b/src/main/java/com/wdf/fudoc/apilist/strategy/ApiGroupStrategy.java new file mode 100644 index 00000000..0393ee54 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/strategy/ApiGroupStrategy.java @@ -0,0 +1,23 @@ +package com.wdf.fudoc.apilist.strategy; + +import com.wdf.fudoc.apilist.pojo.ApiListGroup; +import com.wdf.fudoc.apilist.pojo.ApiListItem; + +import java.util.List; + +/** + * API 分组策略接口 + * + * @author wangdingfu + * @date 2025-01-12 + */ +public interface ApiGroupStrategy { + + /** + * 对 API 列表进行分组 + * + * @param apiList API 列表 + * @return 分组后的结果 + */ + List group(List apiList); +} diff --git a/src/main/java/com/wdf/fudoc/apilist/strategy/ModuleGroupStrategy.java b/src/main/java/com/wdf/fudoc/apilist/strategy/ModuleGroupStrategy.java new file mode 100644 index 00000000..17446802 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/strategy/ModuleGroupStrategy.java @@ -0,0 +1,74 @@ +package com.wdf.fudoc.apilist.strategy; + +import com.google.common.collect.Lists; +import com.wdf.fudoc.apilist.constant.GroupType; +import com.wdf.fudoc.apilist.pojo.ApiListGroup; +import com.wdf.fudoc.apilist.pojo.ApiListItem; +import com.wdf.fudoc.util.FuStringUtils; +import org.apache.commons.collections.CollectionUtils; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 按 Module/Class 分组策略 + * 按照 "Module名称/类名" 进行分组 + * + * @author wangdingfu + * @date 2025-01-12 + */ +public class ModuleGroupStrategy implements ApiGroupStrategy { + + @Override + public List group(List apiList) { + if (CollectionUtils.isEmpty(apiList)) { + return Lists.newArrayList(); + } + + // 按 "moduleName/className" 分组 + Map> groupMap = apiList.stream() + .collect(Collectors.groupingBy( + this::buildGroupKey, + Collectors.toList() + )); + + // 转换为 ApiListGroup 列表 + List groups = Lists.newArrayList(); + groupMap.forEach((groupKey, items) -> { + ApiListGroup group = new ApiListGroup(groupKey, GroupType.MODULE); + items.forEach(group::addItem); + groups.add(group); + }); + + // 按分组名称排序 + groups.sort((g1, g2) -> g1.getGroupName().compareTo(g2.getGroupName())); + + return groups; + } + + /** + * 构建分组键: Module名称/类名 + * 例如: "app-module/UserController" + */ + private String buildGroupKey(ApiListItem api) { + String moduleName = FuStringUtils.isNotBlank(api.getModuleName()) ? api.getModuleName() : "Unknown"; + String className = getSimpleClassName(api.getClassName()); + return moduleName + "/" + className; + } + + /** + * 获取简单类名(不含包名) + * 例如: "com.example.controller.UserController" -> "UserController" + */ + private String getSimpleClassName(String fullClassName) { + if (FuStringUtils.isBlank(fullClassName)) { + return "Unknown"; + } + int lastDot = fullClassName.lastIndexOf('.'); + if (lastDot > 0 && lastDot < fullClassName.length() - 1) { + return fullClassName.substring(lastDot + 1); + } + return fullClassName; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/strategy/PrefixGroupStrategy.java b/src/main/java/com/wdf/fudoc/apilist/strategy/PrefixGroupStrategy.java new file mode 100644 index 00000000..44e160e5 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/strategy/PrefixGroupStrategy.java @@ -0,0 +1,50 @@ +package com.wdf.fudoc.apilist.strategy; + +import com.google.common.collect.Lists; +import com.wdf.fudoc.apilist.constant.GroupType; +import com.wdf.fudoc.apilist.pojo.ApiListGroup; +import com.wdf.fudoc.apilist.pojo.ApiListItem; +import org.apache.commons.collections.CollectionUtils; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 按 URL 前缀分组策略 + * 根据 URL 的第一级路径进行分组,例如: + * - /api/user/list -> /api + * - /user/info -> /user + * + * @author wangdingfu + * @date 2025-01-12 + */ +public class PrefixGroupStrategy implements ApiGroupStrategy { + + @Override + public List group(List apiList) { + if (CollectionUtils.isEmpty(apiList)) { + return Lists.newArrayList(); + } + + // 按 urlPrefix 分组 + Map> groupMap = apiList.stream() + .collect(Collectors.groupingBy( + ApiListItem::getUrlPrefix, + Collectors.toList() + )); + + // 转换为 ApiListGroup 列表 + List groups = Lists.newArrayList(); + groupMap.forEach((prefix, items) -> { + ApiListGroup group = new ApiListGroup(prefix, GroupType.PREFIX); + items.forEach(group::addItem); + groups.add(group); + }); + + // 按分组名称排序 + groups.sort((g1, g2) -> g1.getGroupName().compareTo(g2.getGroupName())); + + return groups; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/ApiItemTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/ApiItemTreeNode.java new file mode 100644 index 00000000..62da1c17 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/ApiItemTreeNode.java @@ -0,0 +1,33 @@ +package com.wdf.fudoc.apilist.tree; + +import com.wdf.fudoc.apilist.pojo.ApiListItem; +import lombok.Getter; + +/** + * API 树节点 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public class ApiItemTreeNode extends ApiTreeNode { + + private final ApiListItem apiItem; + + public ApiItemTreeNode(ApiListItem apiItem) { + super(apiItem, NodeType.API); + this.apiItem = apiItem; + } + + @Override + public String getDisplayText() { + return apiItem.getRequestType().getRequestType() + " " + apiItem.getUrl(); + } + + /** + * 获取右侧显示文本 (标题 + 方法签名) + */ + public String getRightText() { + return apiItem.getDisplayText() + " " + apiItem.getMethodSignature(); + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/ApiListTreeCellRenderer.java b/src/main/java/com/wdf/fudoc/apilist/tree/ApiListTreeCellRenderer.java new file mode 100644 index 00000000..41ad9b13 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/ApiListTreeCellRenderer.java @@ -0,0 +1,175 @@ +package com.wdf.fudoc.apilist.tree; + +import com.intellij.icons.AllIcons; +import com.intellij.ui.ColoredTreeCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import com.wdf.fudoc.apidoc.constant.enumtype.RequestType; +import icons.FuDocIcons; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * API 列表树节点渲染器 + * 自定义树节点的显示样式(图标、颜色、字体等) + * + * @author wangdingfu + * @date 2025-01-12 + */ +public class ApiListTreeCellRenderer extends ColoredTreeCellRenderer { + + @Override + public void customizeCellRenderer(@NotNull JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + if (!(value instanceof ApiTreeNode)) { + return; + } + + ApiTreeNode node = (ApiTreeNode) value; + + switch (node.getNodeType()) { + case ROOT: + renderRootNode(); + break; + case MODULE: + renderModuleNode((ModuleTreeNode) node, expanded); + break; + case PACKAGE: + renderPackageNode((PackageTreeNode) node, expanded); + break; + case CONTROLLER: + renderControllerNode((ControllerTreeNode) node, expanded); + break; + case API: + renderApiNode((ApiItemTreeNode) node); + break; + case GROUP: + renderGroupNode((GroupTreeNode) node, expanded); + break; + } + } + + /** + * 渲染根节点 + */ + private void renderRootNode() { + setIcon(AllIcons.Nodes.Module); + append("所有 API", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + } + + /** + * 渲染 Module 节点 + */ + private void renderModuleNode(ModuleTreeNode node, boolean expanded) { + if (expanded) { + setIcon(AllIcons.Nodes.ModuleGroup); + } else { + setIcon(AllIcons.Nodes.Module); + } + append(node.getModuleName(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + } + + /** + * 渲染 Package 节点 + */ + private void renderPackageNode(PackageTreeNode node, boolean expanded) { + if (expanded) { + setIcon(AllIcons.Nodes.Package); + } else { + setIcon(AllIcons.Nodes.Package); + } + append(node.getPackageName(), SimpleTextAttributes.REGULAR_ATTRIBUTES); + } + + /** + * 渲染 Controller 节点 + */ + private void renderControllerNode(ControllerTreeNode node, boolean expanded) { + setIcon(AllIcons.Nodes.Class); + append(node.getSimpleClassName(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + } + + /** + * 渲染分组节点 + */ + private void renderGroupNode(GroupTreeNode node, boolean expanded) { + // 设置图标(展开/折叠) + if (expanded) { + setIcon(AllIcons.Nodes.ModuleGroup); + } else { + setIcon(AllIcons.Nodes.Module); + } + + // 显示分组名称 + append(node.getGroup().getGroupName(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES); + + // 显示 API 数量 + append(" (" + node.getGroup().getItemCount() + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + + /** + * 渲染 API 节点 + */ + private void renderApiNode(ApiItemTreeNode node) { + RequestType requestType = node.getApiItem().getRequestType(); + + // 设置图标 + setIcon(getRequestTypeIcon(requestType)); + + // 显示请求类型 (带颜色) + SimpleTextAttributes typeAttributes = getRequestTypeAttributes(requestType); + append(requestType.getRequestType(), typeAttributes); + + // 显示 URL + append(" " + node.getApiItem().getUrl(), SimpleTextAttributes.REGULAR_ATTRIBUTES); + + // 显示标题和方法签名(灰色) + String rightText = " " + node.getApiItem().getDisplayText() + " " + node.getApiItem().getMethodSignature(); + append(rightText, SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + + /** + * 根据请求类型获取图标 + */ + private Icon getRequestTypeIcon(RequestType requestType) { + switch (requestType) { + case GET: + return AllIcons.Actions.Download; + case POST: + return AllIcons.Actions.Upload; + case PUT: + return AllIcons.Actions.Edit; + case DELETE: + return AllIcons.Actions.Cancel; + default: + return FuDocIcons.HTTP; + } + } + + /** + * 根据请求类型获取文本属性(颜色) + */ + private SimpleTextAttributes getRequestTypeAttributes(RequestType requestType) { + switch (requestType) { + case GET: + return new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, + new java.awt.Color(0, 128, 0)); // 绿色 + case POST: + return new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, + new java.awt.Color(255, 140, 0)); // 橙色 + case PUT: + return new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, + new java.awt.Color(0, 0, 255)); // 蓝色 + case DELETE: + return new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, + new java.awt.Color(255, 0, 0)); // 红色 + default: + return SimpleTextAttributes.REGULAR_ATTRIBUTES; + } + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/ApiTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/ApiTreeNode.java new file mode 100644 index 00000000..59b40fc7 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/ApiTreeNode.java @@ -0,0 +1,39 @@ +package com.wdf.fudoc.apilist.tree; + +import lombok.Getter; + +import javax.swing.tree.DefaultMutableTreeNode; + +/** + * API 列表树节点基类 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public abstract class ApiTreeNode extends DefaultMutableTreeNode { + + /** + * 节点类型 + */ + public enum NodeType { + ROOT, // 根节点 + MODULE, // Module 节点 + PACKAGE, // Package 节点 + CONTROLLER, // Controller 节点 + GROUP, // 通用分组节点(用于 Prefix 分组等) + API // API 节点 + } + + private final NodeType nodeType; + + public ApiTreeNode(Object userObject, NodeType nodeType) { + super(userObject); + this.nodeType = nodeType; + } + + /** + * 获取节点显示文本 + */ + public abstract String getDisplayText(); +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/ControllerTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/ControllerTreeNode.java new file mode 100644 index 00000000..0e83b9bf --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/ControllerTreeNode.java @@ -0,0 +1,41 @@ +package com.wdf.fudoc.apilist.tree; + +import lombok.Getter; + +/** + * Controller 树节点 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public class ControllerTreeNode extends ApiTreeNode { + + private final String className; + private final String simpleClassName; + + public ControllerTreeNode(String className) { + super(className, NodeType.CONTROLLER); + this.className = className; + this.simpleClassName = extractSimpleClassName(className); + } + + @Override + public String getDisplayText() { + return simpleClassName; + } + + /** + * 提取简单类名 + */ + private String extractSimpleClassName(String fullClassName) { + if (fullClassName == null || fullClassName.isEmpty()) { + return "Unknown"; + } + int lastDot = fullClassName.lastIndexOf('.'); + if (lastDot > 0 && lastDot < fullClassName.length() - 1) { + return fullClassName.substring(lastDot + 1); + } + return fullClassName; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/GroupTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/GroupTreeNode.java new file mode 100644 index 00000000..3dc9d0e9 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/GroupTreeNode.java @@ -0,0 +1,26 @@ +package com.wdf.fudoc.apilist.tree; + +import com.wdf.fudoc.apilist.pojo.ApiListGroup; +import lombok.Getter; + +/** + * API 分组树节点 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public class GroupTreeNode extends ApiTreeNode { + + private final ApiListGroup group; + + public GroupTreeNode(ApiListGroup group) { + super(group, NodeType.GROUP); + this.group = group; + } + + @Override + public String getDisplayText() { + return group.getDisplayText(); + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/ModuleTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/ModuleTreeNode.java new file mode 100644 index 00000000..d7c00969 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/ModuleTreeNode.java @@ -0,0 +1,25 @@ +package com.wdf.fudoc.apilist.tree; + +import lombok.Getter; + +/** + * Module 树节点 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public class ModuleTreeNode extends ApiTreeNode { + + private final String moduleName; + + public ModuleTreeNode(String moduleName) { + super(moduleName, NodeType.MODULE); + this.moduleName = moduleName; + } + + @Override + public String getDisplayText() { + return moduleName; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/tree/PackageTreeNode.java b/src/main/java/com/wdf/fudoc/apilist/tree/PackageTreeNode.java new file mode 100644 index 00000000..ce37182e --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/tree/PackageTreeNode.java @@ -0,0 +1,25 @@ +package com.wdf.fudoc.apilist.tree; + +import lombok.Getter; + +/** + * Package 树节点 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Getter +public class PackageTreeNode extends ApiTreeNode { + + private final String packageName; + + public PackageTreeNode(String packageName) { + super(packageName, NodeType.PACKAGE); + this.packageName = packageName; + } + + @Override + public String getDisplayText() { + return packageName; + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindow.java b/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindow.java new file mode 100644 index 00000000..8cc4a21b --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindow.java @@ -0,0 +1,653 @@ +package com.wdf.fudoc.apilist.view; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.ui.SearchTextField; +import com.intellij.ui.TreeSpeedSearch; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.ui.treeStructure.Tree; +import com.intellij.util.ui.tree.TreeUtil; +import com.wdf.fudoc.apilist.constant.GroupType; +import com.wdf.fudoc.apilist.pojo.ApiListGroup; +import com.wdf.fudoc.apilist.pojo.ApiListItem; +import com.wdf.fudoc.apilist.service.ApiListCollector; +import com.wdf.fudoc.apilist.strategy.ApiGroupStrategy; +import com.wdf.fudoc.apilist.strategy.ModuleGroupStrategy; +import com.wdf.fudoc.apilist.strategy.PrefixGroupStrategy; +import com.wdf.fudoc.apilist.tree.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * API 列表工具窗口主面板 + * + * @author wangdingfu + * @date 2025-01-12 + */ +@Slf4j +public class ApiListToolWindow extends SimpleToolWindowPanel { + + private final Project project; + + @Getter + private final Tree tree; + + private final ApiTreeNode rootNode; + private final DefaultTreeModel treeModel; + + private SearchTextField searchField; + private GroupType currentGroupType = GroupType.MODULE; + + public ApiListToolWindow(@NotNull Project project) { + super(true, true); + this.project = project; + + // 创建根节点和树模型 + this.rootNode = new ApiTreeNode(null, ApiTreeNode.NodeType.ROOT) { + @Override + public String getDisplayText() { + return "所有 API"; + } + }; + this.treeModel = new DefaultTreeModel(rootNode); + this.tree = new Tree(treeModel); + + // 初始化 UI + initUI(); + + // 异步加载 API 数据 + loadApis(); + } + + /** + * 初始化 UI 组件 + */ + private void initUI() { + // 设置树的渲染器 + tree.setCellRenderer(new ApiListTreeCellRenderer()); + + // 设置树的根节点可见 + tree.setRootVisible(false); + tree.setShowsRootHandles(true); + + // 启用快速搜索 + new TreeSpeedSearch(tree, treePath -> { + Object node = treePath.getLastPathComponent(); + if (node instanceof ApiTreeNode) { + return ((ApiTreeNode) node).getDisplayText(); + } + return ""; + }); + + // 添加双击监听器(跳转到源码) + tree.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (e.getClickCount() == 2) { + handleDoubleClick(); + } + } + }); + + // 添加右键菜单 + tree.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mousePressed(java.awt.event.MouseEvent e) { + if (e.isPopupTrigger()) { + showContextMenu(e); + } + } + + @Override + public void mouseReleased(java.awt.event.MouseEvent e) { + if (e.isPopupTrigger()) { + showContextMenu(e); + } + } + }); + + // 创建工具栏 + JPanel toolbarPanel = createToolbarPanel(); + + // 创建滚动面板 + JBScrollPane scrollPane = new JBScrollPane(tree); + + // 布局 + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.add(toolbarPanel, BorderLayout.NORTH); + contentPanel.add(scrollPane, BorderLayout.CENTER); + + setContent(contentPanel); + } + + /** + * 创建工具栏面板 + */ + private JPanel createToolbarPanel() { + JPanel panel = new JPanel(new BorderLayout()); + + // 创建搜索框 + searchField = new SearchTextField(true); + searchField.addDocumentListener(new javax.swing.event.DocumentListener() { + @Override + public void insertUpdate(javax.swing.event.DocumentEvent e) { + filterTree(); + } + + @Override + public void removeUpdate(javax.swing.event.DocumentEvent e) { + filterTree(); + } + + @Override + public void changedUpdate(javax.swing.event.DocumentEvent e) { + filterTree(); + } + }); + + // 创建操作按钮组 + DefaultActionGroup actionGroup = new DefaultActionGroup(); + + // 刷新按钮 + actionGroup.add(new AnAction("刷新", "重新加载所有 API", AllIcons.Actions.Refresh) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + loadApis(); + } + }); + + // 分组方式切换按钮 + actionGroup.add(new AnAction("按模块分组", "按 Module 分组 API", AllIcons.Nodes.Module) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + switchGroupType(GroupType.MODULE); + } + }); + + actionGroup.add(new AnAction("按前缀分组", "按 URL 前缀分组 API", AllIcons.Nodes.Folder) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + switchGroupType(GroupType.PREFIX); + } + }); + + // 展开/折叠按钮 + actionGroup.add(new AnAction("展开全部", "展开所有分组", AllIcons.Actions.Expandall) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + TreeUtil.expandAll(tree); + } + }); + + actionGroup.add(new AnAction("折叠全部", "折叠所有分组", AllIcons.Actions.Collapseall) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + TreeUtil.collapseAll(tree, 0); + } + }); + + ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("ApiListToolbar", actionGroup, true); + toolbar.setTargetComponent(this); + + panel.add(searchField, BorderLayout.NORTH); + panel.add(toolbar.getComponent(), BorderLayout.CENTER); + + return panel; + } + + /** + * 加载 API 列表 + */ + private void loadApis() { + // 清空当前树 + rootNode.removeAllChildren(); + treeModel.reload(); + + // 在后台线程收集 API + ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + // 收集 API + ApiListCollector collector = ApiListCollector.getInstance(project); + List apiList = collector.collectAllApis(); + + // 在 EDT 线程更新 UI + ApplicationManager.getApplication().invokeLater(() -> { + if (currentGroupType == GroupType.MODULE) { + buildModuleTree(apiList); + } else { + buildPrefixTree(apiList); + } + }); + } catch (Exception e) { + log.error("加载 API 列表失败", e); + } + }); + } + + /** + * 构建 Module 层级树 (VSCode 风格的包路径展示) + * 规则:叶子包合并(中间只有一条路径的包节点合并显示) + */ + private void buildModuleTree(List apiList) { + rootNode.removeAllChildren(); + + // 1. 按 Module 分组 + Map> moduleMap = apiList.stream() + .collect(Collectors.groupingBy( + api -> api.getModuleName() != null ? api.getModuleName() : "Unknown", + Collectors.toList() + )); + + // 2. 为每个 Module 构建包树 + moduleMap.forEach((moduleName, moduleApis) -> { + ModuleTreeNode moduleNode = new ModuleTreeNode(moduleName); + rootNode.add(moduleNode); + + // 3. 按包名分组所有 Controller + Map> packageMap = moduleApis.stream() + .collect(Collectors.groupingBy( + this::extractPackageName, + Collectors.toList() + )); + + // 4. 构建包树(带路径压缩) + buildPackageTree(moduleNode, packageMap); + }); + + // 刷新树并展开到 Controller 层(不展开 API 列表) + treeModel.reload(); + expandToControllerLevel(); + } + + /** + * 展开树到 Controller 层级(不展开 API 列表) + * 递归展开所有非 API 节点 + */ + private void expandToControllerLevel() { + // 从根节点开始递归展开 + expandNodeExceptApiLevel(new TreePath(rootNode)); + } + + /** + * 递归展开节点,但不展开 Controller 节点(即不显示 API 列表) + */ + private void expandNodeExceptApiLevel(TreePath path) { + Object node = path.getLastPathComponent(); + if (!(node instanceof ApiTreeNode)) { + return; + } + + ApiTreeNode apiNode = (ApiTreeNode) node; + + // 如果是 Controller 节点,不展开(保持 API 列表折叠) + if (apiNode.getNodeType() == ApiTreeNode.NodeType.CONTROLLER) { + return; + } + + // 展开当前节点 + tree.expandPath(path); + + // 递归展开所有子节点 + int childCount = apiNode.getChildCount(); + for (int i = 0; i < childCount; i++) { + Object child = apiNode.getChildAt(i); + TreePath childPath = path.pathByAddingChild(child); + expandNodeExceptApiLevel(childPath); + } + } + + /** + * 构建包树(VSCode 风格:叶子包合并) + * + * 新算法: + * 1. 构建完整的包树结构(Trie树) + * 2. 标记哪些包节点包含 Controller + * 3. 应用路径压缩:如果某节点只有一个子节点且不包含 Controller,则与子节点合并 + */ + private void buildPackageTree(ModuleTreeNode moduleNode, Map> packageMap) { + // 第一步:构建包树节点结构 + PackageTreeBuilder treeBuilder = new PackageTreeBuilder(); + + // 添加所有包路径和对应的 API + packageMap.forEach(treeBuilder::addPackage); + + // 第二步:构建压缩后的树,并添加到 moduleNode + treeBuilder.buildCompressedTree(moduleNode); + } + + /** + * 包树构建器(内部类) + * 负责构建 VSCode 风格的包树结构 + */ + private class PackageTreeBuilder { + // 包节点映射:完整包名 -> 包数据 + private final Map packageDataMap = new java.util.HashMap<>(); + + /** + * 包节点数据 + */ + private class PackageNodeData { + String fullPackageName; + List apis = new ArrayList<>(); // 此包下的 API(如果有) + Set childPackages = new java.util.HashSet<>(); // 直接子包的完整名称 + + PackageNodeData(String fullPackageName) { + this.fullPackageName = fullPackageName; + } + } + + /** + * 添加包及其 API + */ + void addPackage(String fullPackageName, List apis) { + // 创建或获取当前包节点 + PackageNodeData currentData = packageDataMap.computeIfAbsent( + fullPackageName, + PackageNodeData::new + ); + currentData.apis.addAll(apis); + + // 创建所有父包节点(如果不存在) + String[] parts = fullPackageName.split("\\."); + for (int i = 0; i < parts.length - 1; i++) { + String parentPackage = String.join(".", java.util.Arrays.copyOfRange(parts, 0, i + 1)); + String childPackage = String.join(".", java.util.Arrays.copyOfRange(parts, 0, i + 2)); + + PackageNodeData parentData = packageDataMap.computeIfAbsent( + parentPackage, + PackageNodeData::new + ); + parentData.childPackages.add(childPackage); + } + } + + /** + * 构建压缩后的树 + */ + void buildCompressedTree(ModuleTreeNode moduleNode) { + // 找出所有根包(没有父包的包) + Set rootPackages = findRootPackages(); + + // 为每个根包构建子树(父路径为空) + for (String rootPackage : rootPackages) { + buildSubTree(rootPackage, "", moduleNode); + } + } + + /** + * 找出所有根包 + */ + private Set findRootPackages() { + Set allPackages = new java.util.HashSet<>(packageDataMap.keySet()); + Set nonRootPackages = new java.util.HashSet<>(); + + // 移除所有是其他包子包的包 + for (PackageNodeData data : packageDataMap.values()) { + nonRootPackages.addAll(data.childPackages); + } + + allPackages.removeAll(nonRootPackages); + return allPackages; + } + + /** + * 递归构建子树(带路径压缩) + * + * @param packageName 当前包的完整名称 + * @param parentPackagePath 父包的完整路径(用于计算显示名称) + * @param parentNode 父树节点 + */ + private void buildSubTree(String packageName, String parentPackagePath, javax.swing.tree.DefaultMutableTreeNode parentNode) { + PackageNodeData data = packageDataMap.get(packageName); + if (data == null) { + return; + } + + // 查找压缩路径:如果只有一个子包且当前包没有 Controller,继续合并 + String compressedPackageName = findCompressedPath(packageName); + PackageNodeData compressedData = packageDataMap.get(compressedPackageName); + + // 计算显示名称(相对于父包的路径) + String displayName; + if (parentPackagePath.isEmpty()) { + // 根包:显示完整压缩路径 + displayName = compressedPackageName; + } else { + // 子包:只显示相对于父包的路径 + if (compressedPackageName.startsWith(parentPackagePath + ".")) { + displayName = compressedPackageName.substring(parentPackagePath.length() + 1); + } else { + displayName = compressedPackageName; + } + } + + // 创建包节点 + PackageTreeNode packageNode = new PackageTreeNode(displayName); + parentNode.add(packageNode); + + // 添加此包下的所有 Controller + if (compressedData != null && !compressedData.apis.isEmpty()) { + Map> controllerMap = compressedData.apis.stream() + .collect(Collectors.groupingBy(ApiListItem::getClassName)); + + controllerMap.forEach((className, controllerApis) -> { + ControllerTreeNode controllerNode = new ControllerTreeNode(className); + packageNode.add(controllerNode); + + // 添加 API 节点 + controllerApis.forEach(apiItem -> { + ApiItemTreeNode apiNode = new ApiItemTreeNode(apiItem); + controllerNode.add(apiNode); + }); + }); + } + + // 递归构建子包(使用压缩后的完整路径作为新的父路径) + if (compressedData != null) { + for (String childPackage : compressedData.childPackages) { + buildSubTree(childPackage, compressedPackageName, packageNode); + } + } + } + + /** + * 查找压缩路径 + * 如果当前包只有一个子包且没有 Controller,则与子包合并 + */ + private String findCompressedPath(String packageName) { + String current = packageName; + + while (true) { + PackageNodeData data = packageDataMap.get(current); + if (data == null) { + break; + } + + // 如果有 API 或有多个子包,停止压缩 + if (!data.apis.isEmpty() || data.childPackages.size() != 1) { + break; + } + + // 继续压缩到唯一的子包 + current = data.childPackages.iterator().next(); + } + + return current; + } + } + + /** + * 构建 Prefix 扁平树 (两级: Prefix → API) + */ + private void buildPrefixTree(List apiList) { + rootNode.removeAllChildren(); + + // 按 URL 前缀分组 + ApiGroupStrategy strategy = new PrefixGroupStrategy(); + List groups = strategy.group(apiList); + + for (ApiListGroup group : groups) { + // 创建分组节点 + GroupTreeNode groupNode = new GroupTreeNode(group); + rootNode.add(groupNode); + + // 添加 API 节点 + for (ApiListItem apiItem : group.getItems()) { + ApiItemTreeNode apiNode = new ApiItemTreeNode(apiItem); + groupNode.add(apiNode); + } + } + + // 刷新树并展开到分组层(不展开 API 列表) + treeModel.reload(); + expandToControllerLevel(); + } + + /** + * 提取包名 (不含类名) + * 例如: "com.example.controller.UserController" -> "com.example.controller" + */ + private String extractPackageName(ApiListItem api) { + String className = api.getClassName(); + if (className == null || className.isEmpty()) { + return "default"; + } + int lastDot = className.lastIndexOf('.'); + if (lastDot > 0) { + return className.substring(0, lastDot); + } + return "default"; + } + + /** + * 切换分组方式 + */ + private void switchGroupType(GroupType groupType) { + if (this.currentGroupType != groupType) { + this.currentGroupType = groupType; + loadApis(); + } + } + + /** + * 获取分组策略 + */ + private ApiGroupStrategy getGroupStrategy(GroupType groupType) { + switch (groupType) { + case PREFIX: + return new PrefixGroupStrategy(); + case MODULE: + default: + return new ModuleGroupStrategy(); + } + } + + /** + * 过滤树节点 + */ + private void filterTree() { + String keyword = searchField.getText().trim().toLowerCase(); + if (keyword.isEmpty()) { + // 清空搜索,重新加载 + loadApis(); + return; + } + + // TODO: 实现搜索过滤逻辑 + // 暂时简单实现:遍历所有节点,隐藏不匹配的节点 + } + + /** + * 处理双击事件(跳转到源码) + */ + private void handleDoubleClick() { + TreePath path = tree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ApiItemTreeNode) { + ApiItemTreeNode apiNode = (ApiItemTreeNode) node; + navigateToSource(apiNode.getApiItem()); + } + } + + /** + * 显示右键菜单 + */ + private void showContextMenu(java.awt.event.MouseEvent e) { + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path == null) { + return; + } + + tree.setSelectionPath(path); + Object node = path.getLastPathComponent(); + + if (node instanceof ApiItemTreeNode) { + ApiItemTreeNode apiNode = (ApiItemTreeNode) node; + JPopupMenu menu = createApiContextMenu(apiNode.getApiItem()); + menu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + /** + * 创建 API 节点的右键菜单 + */ + private JPopupMenu createApiContextMenu(ApiListItem apiItem) { + JPopupMenu menu = new JPopupMenu(); + + // 跳转到源码 + JMenuItem navigateItem = new JMenuItem("跳转到源码", AllIcons.Actions.EditSource); + navigateItem.addActionListener(e -> navigateToSource(apiItem)); + menu.add(navigateItem); + + menu.addSeparator(); + + // 复制 URL + JMenuItem copyUrlItem = new JMenuItem("复制 URL", AllIcons.Actions.Copy); + copyUrlItem.addActionListener(e -> copyToClipboard(apiItem.getUrl())); + menu.add(copyUrlItem); + + // 复制完整路径 + String fullUrl = apiItem.getUrl(); + JMenuItem copyFullUrlItem = new JMenuItem("复制完整 URL", AllIcons.Actions.Copy); + copyFullUrlItem.addActionListener(e -> copyToClipboard(fullUrl)); + menu.add(copyFullUrlItem); + + return menu; + } + + /** + * 跳转到源码 + */ + private void navigateToSource(ApiListItem apiItem) { + ApplicationManager.getApplication().invokeLater(() -> { + apiItem.getPsiMethod().navigate(true); + }); + } + + /** + * 复制到剪贴板 + */ + private void copyToClipboard(String text) { + java.awt.Toolkit.getDefaultToolkit() + .getSystemClipboard() + .setContents(new java.awt.datatransfer.StringSelection(text), null); + } +} diff --git a/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindowFactory.java b/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindowFactory.java new file mode 100644 index 00000000..cd8a27b2 --- /dev/null +++ b/src/main/java/com/wdf/fudoc/apilist/view/ApiListToolWindowFactory.java @@ -0,0 +1,29 @@ +package com.wdf.fudoc.apilist.view; + +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import org.jetbrains.annotations.NotNull; + +/** + * API 列表工具窗口工厂 + * + * @author wangdingfu + * @date 2025-01-12 + */ +public class ApiListToolWindowFactory implements ToolWindowFactory, DumbAware { + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + // 创建 API 列表面板 + ApiListToolWindow apiListToolWindow = new ApiListToolWindow(project); + + // 创建 Content 并添加到 ToolWindow + ContentFactory contentFactory = ContentFactory.getInstance(); + Content content = contentFactory.createContent(apiListToolWindow, "", false); + toolWindow.getContentManager().addContent(content); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c756768d..1d88ad33 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -93,6 +93,14 @@ order="last" factoryClass="com.wdf.fudoc.request.view.toolwindow.FuDocToolWindowFactory"/> + +