Compare commits
12 Commits
cbe41e4f4f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3faaa62e12 | |||
| e031be8437 | |||
| 105584f118 | |||
| 90ff9b107e | |||
| 47f3129dfb | |||
| 3fd4a01b4b | |||
| e51c046007 | |||
| b3fee364e0 | |||
| dfda6042e7 | |||
| e795320845 | |||
| c9c6744cc0 | |||
| 4890569b95 |
+11
-14
@@ -1,15 +1,12 @@
|
||||
# Gradle
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.gradle
|
||||
.idea
|
||||
.intellijPlatform
|
||||
build
|
||||
node_modules
|
||||
web/dist
|
||||
*.iml
|
||||
.vscode/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
web/dist/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
scripts/
|
||||
tools/
|
||||
.tools/
|
||||
release/
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run IDE with Plugin" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log/idea.log" show_all="true" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runIde" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log-test/idea.log" show_all="true" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="check" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>true</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,25 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Verifications" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log/idea.log" show_all="true" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="verifyPlugin" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
|
||||
|
||||
# Harmness-browser Changelog
|
||||
|
||||
## [Unreleased]
|
||||
+7
-6
@@ -1,11 +1,12 @@
|
||||
import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("org.jetbrains.kotlin.jvm") version "2.0.21"
|
||||
id("org.jetbrains.intellij.platform") version "2.5.1"
|
||||
id("org.jetbrains.kotlin.jvm")
|
||||
id("org.jetbrains.intellij.platform")
|
||||
}
|
||||
|
||||
group = "com.harmness"
|
||||
version = "0.1.0"
|
||||
group = "com.prepeep"
|
||||
version = "1.0.3"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -26,7 +27,7 @@ intellijPlatform {
|
||||
pluginConfiguration {
|
||||
ideaVersion {
|
||||
sinceBuild = "242"
|
||||
untilBuild = "243.*"
|
||||
untilBuild = "263.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,2 +1,9 @@
|
||||
group = com.prepeep
|
||||
version = 1.0.0-SNAPSHOT
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=false
|
||||
org.gradle.parallel=false
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
|
||||
kotlin.stdlib.default.dependency = false
|
||||
@@ -0,0 +1,5 @@
|
||||
[versions]
|
||||
junit = "4.13.2"
|
||||
|
||||
[libraries]
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
Vendored
BIN
Binary file not shown.
+9
@@ -0,0 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
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
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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, 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" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# 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.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 准备本地构建工具链(无 root 安装,全部放到工作区 .tools 下)
|
||||
set -euo pipefail
|
||||
|
||||
TOOLS_DIR="/data/project/Plugin/harmness/.tools"
|
||||
mkdir -p "$TOOLS_DIR"
|
||||
cd "$TOOLS_DIR"
|
||||
|
||||
JDK_VERSION="jdk-17.0.12+7"
|
||||
GRADLE_VERSION="gradle-8.11.1"
|
||||
|
||||
echo "==> Downloading Temurin JDK17 ..."
|
||||
curl -fL --retry 3 -o jdk17.tar.gz \
|
||||
"https://github.com/adoptium/temurin17-binaries/releases/download/${JDK_VERSION}/OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz"
|
||||
tar xzf jdk17.tar.gz
|
||||
chmod +x jdk-17.0.12+7/bin/java
|
||||
"$TOOLS_DIR/jdk-17.0.12+7/bin/java" -version
|
||||
|
||||
echo "==> Downloading Gradle $GRADLE_VERSION ..."
|
||||
curl -fL --retry 3 -o gradle.zip \
|
||||
"https://services.gradle.org/distributions/${GRADLE_VERSION}-bin.zip"
|
||||
unzip -q gradle.zip
|
||||
chmod +x "$GRADLE_VERSION/bin/gradle"
|
||||
JAVA_HOME="$TOOLS_DIR/jdk-17.0.12+7" "$GRADLE_VERSION/bin/gradle" --version
|
||||
|
||||
echo "==> DONE"
|
||||
+12
-1
@@ -1 +1,12 @@
|
||||
rootProject.name = "harmness-toolwindow"
|
||||
pluginManagement {
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "2.0.21"
|
||||
id("org.jetbrains.intellij.platform") version "2.1.0"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "harmness-browser"
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* JCEF 浏览器的纯反射封装。
|
||||
*
|
||||
* 目标:让插件代码在**编译期完全不引用** com.intellij.ui.jcef.*
|
||||
* (以及 org.cef.*),避免 IDE 缺少 JCEF 类时出现
|
||||
* NoClassDefFoundError 把整个面板拖垮。
|
||||
*
|
||||
* 所有 JCEF 调用都走反射;类不存在或调用失败时返回 null / 静默降级。
|
||||
*/
|
||||
class EmbeddedBrowser internal constructor(val instance: Any) {
|
||||
|
||||
/** 浏览器 Swing 组件(可加入面板);失败返回 null */
|
||||
val component: JComponent?
|
||||
get() = try {
|
||||
instance.invoke("getComponent") as? JComponent
|
||||
} catch (t: Throwable) {
|
||||
log.warn("getComponent failed", t)
|
||||
null
|
||||
}
|
||||
|
||||
fun load(url: String): Boolean = try {
|
||||
instance.invoke("loadURL", url)
|
||||
true
|
||||
} catch (t: Throwable) {
|
||||
log.warn("loadURL failed for $url", t)
|
||||
false
|
||||
}
|
||||
|
||||
fun canGoBack(): Boolean = cefCall("canGoBack") as? Boolean ?: false
|
||||
|
||||
fun goBack(): Boolean = cefCall("goBack") != null
|
||||
|
||||
fun canGoForward(): Boolean = cefCall("canGoForward") as? Boolean ?: false
|
||||
|
||||
fun goForward(): Boolean = cefCall("goForward") != null
|
||||
|
||||
fun reload(): Boolean = cefCall("reload") != null
|
||||
|
||||
fun dispose() {
|
||||
try {
|
||||
instance.invoke("dispose")
|
||||
} catch (t: Throwable) {
|
||||
log.warn("browser dispose failed", t)
|
||||
}
|
||||
}
|
||||
|
||||
// org.cef.browser.CefBrowser 上的方法(getCefBrowser() 返回它)
|
||||
private fun cefCall(name: String, vararg args: Any?): Any? = try {
|
||||
val cef = instance.invoke("getCefBrowser") ?: return null
|
||||
cef.invoke(name, *args)
|
||||
} catch (t: Throwable) {
|
||||
log.warn("cefCall $name failed", t)
|
||||
null
|
||||
}
|
||||
|
||||
private fun Any.invoke(method: String, vararg args: Any?): Any? {
|
||||
val clazz = this::class.java
|
||||
return try {
|
||||
for (m in clazz.methods) {
|
||||
if (m.name == method && m.parameterCount == args.size) {
|
||||
m.isAccessible = true
|
||||
return m.invoke(this, *args)
|
||||
}
|
||||
}
|
||||
// 按名字+精确参数类型再找一次
|
||||
val types = args.map { it?.javaClass ?: String::class.java }.toTypedArray()
|
||||
val m = clazz.getMethod(method, *types)
|
||||
m.isAccessible = true
|
||||
m.invoke(this, *args)
|
||||
} catch (t: Throwable) {
|
||||
log.warn("reflect invoke $method failed", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = Logger.getInstance(EmbeddedBrowser::class.java)
|
||||
|
||||
/** IDE 是否提供可用 JCEF(JBCefApp.isSupported 的反射版本) */
|
||||
fun isJcefSupported(): Boolean = try {
|
||||
val app = Class.forName("com.intellij.ui.jcef.JBCefApp")
|
||||
val m = app.getMethod("isSupported")
|
||||
m.invoke(null) as? Boolean ?: false
|
||||
} catch (t: Throwable) {
|
||||
log.info("JCEF not supported in this IDE: ${t.javaClass.simpleName}")
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试创建嵌入式浏览器。
|
||||
* @return 成功返回封装对象;失败(类缺失 / 不支持 / 构造异常)返回 null
|
||||
*/
|
||||
fun create(): EmbeddedBrowser? {
|
||||
try {
|
||||
val cls = Class.forName("com.intellij.ui.jcef.JBCefBrowser")
|
||||
val ctor = cls.getConstructor()
|
||||
val inst = ctor.newInstance()
|
||||
// 验证 getComponent() 可用,避免构造成功但组件拿不到
|
||||
inst.javaClass.getMethod("getComponent").invoke(inst)
|
||||
return EmbeddedBrowser(inst)
|
||||
} catch (t: Throwable) {
|
||||
val real = when (t) {
|
||||
is InvocationTargetException -> t.targetException ?: t
|
||||
else -> t
|
||||
}
|
||||
log.warn("EmbeddedBrowser.create failed: ${real.javaClass.simpleName}: ${real.message}", real)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.function.Function
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* JCEF JS↔Java 桥的反射封装。
|
||||
*
|
||||
* 负责两件事:
|
||||
* 1. 通过反射创建 JBCefJSQuery 并注册 handler(把网页发来的字符串请求转发给 [onRequest])
|
||||
* 2. 通过反射调用 JBCefJSQuery.inject() 生成注入到页面的 JS 代码
|
||||
*
|
||||
* 与 [EmbeddedBrowser] 一样,编译期零 JCEF 依赖。类缺失/调用失败时
|
||||
* 仅返回 null / 空串,不影响浏览器主体功能。
|
||||
*/
|
||||
class EmbeddedJsBridge private constructor(
|
||||
private val queryInstance: Any,
|
||||
private val onRequest: (String) -> Any?
|
||||
) {
|
||||
|
||||
/**
|
||||
* 返回可注入到页面 JS 的代码片段(调用插件 handler),
|
||||
* 失败返回空串。
|
||||
*/
|
||||
fun injectJs(): String = try {
|
||||
val m = queryInstance.javaClass.getMethod(
|
||||
"inject", String::class.java, String::class.java, String::class.java
|
||||
)
|
||||
(m.invoke(queryInstance, "payload.query",
|
||||
"function(response){ if (payload.callbacks && payload.callbacks.onSuccess) payload.callbacks.onSuccess(response); }",
|
||||
"function(code,msg){ if (payload.callbacks && payload.callbacks.onFailure) payload.callbacks.onFailure(code,msg); }"
|
||||
) as? String) ?: ""
|
||||
} catch (t: Throwable) {
|
||||
log.warn("injectJs failed", t)
|
||||
""
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
try {
|
||||
queryInstance.javaClass.getMethod("dispose").invoke(queryInstance)
|
||||
} catch (t: Throwable) {
|
||||
log.warn("bridge dispose failed", t)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = Logger.getInstance(EmbeddedJsBridge::class.java)
|
||||
|
||||
/**
|
||||
* 为已创建的 [browser] 建立 JS 桥。
|
||||
* @return 成功返回桥对象;失败返回 null(浏览器仍可用,仅网页无法调用插件 API)
|
||||
*/
|
||||
fun create(browser: EmbeddedBrowser, onRequest: (String) -> Any?): EmbeddedJsBridge? {
|
||||
try {
|
||||
val queryClass = Class.forName("com.intellij.ui.jcef.JBCefJSQuery")
|
||||
// create(JBCefBrowserBase) — 用可赋值参数类型查找
|
||||
val browserInst = browser.instance
|
||||
val createMethod = queryClass.methods.firstOrNull { m ->
|
||||
m.name == "create" && m.parameterCount == 1 &&
|
||||
m.parameterTypes[0].isAssignableFrom(browserInst.javaClass)
|
||||
} ?: return null
|
||||
val queryInstance = createMethod.invoke(null, browserInst)
|
||||
|
||||
// addHandler(java.util.function.Function<String, Response>)
|
||||
val handlerFn = Function<String, Any?> { req -> onRequest(req ?: "") }
|
||||
val addMethod = queryClass.methods.firstOrNull { m ->
|
||||
m.name == "addHandler" && m.parameterCount == 1 &&
|
||||
m.parameterTypes[0] == Function::class.java
|
||||
} ?: return null
|
||||
addMethod.invoke(queryInstance, handlerFn)
|
||||
|
||||
return EmbeddedJsBridge(queryInstance, onRequest)
|
||||
} catch (t: Throwable) {
|
||||
val real = when (t) {
|
||||
is InvocationTargetException -> t.targetException ?: t
|
||||
else -> t
|
||||
}
|
||||
log.warn("EmbeddedJsBridge.create failed: ${real.javaClass.simpleName}: ${real.message}", real)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* 设置面板 UI:编辑「起始地址」并即时生效。
|
||||
*/
|
||||
class HarmnessBrowserConfigurablePanel {
|
||||
|
||||
private val urlField = JBTextField()
|
||||
|
||||
val panel: JPanel = FormBuilder.createFormBuilder()
|
||||
.addLabeledComponent("Start URL", urlField, 1, false)
|
||||
.addComponentFillVertically(JBLabel(
|
||||
"<html>Embedded browser will open this URL when the tool window is shown.<br>" +
|
||||
"The target site must allow embedding (no strict <code>X-Frame-Options</code> / <code>CSP frame-ancestors</code>).<br>" +
|
||||
"e.g. <b>http://127.0.0.1:8888</b></html>", UIUtil.ComponentStyle.SMALL, UIUtil.FontColor.NORMAL), 1)
|
||||
.panel
|
||||
|
||||
init {
|
||||
urlField.columns = 40
|
||||
urlField.emptyText.text = "http://127.0.0.1:8888"
|
||||
}
|
||||
|
||||
fun isModified(settings: SettingsState): Boolean =
|
||||
urlField.text != settings.startUrl
|
||||
|
||||
fun apply(settings: SettingsState) {
|
||||
settings.startUrl = urlField.text.trim()
|
||||
}
|
||||
|
||||
fun reset(settings: SettingsState) {
|
||||
urlField.text = settings.startUrl
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance(HarmnessBrowserConfigurablePanel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.ui.jcef.JBCefApp
|
||||
import com.intellij.ui.jcef.JBCefBrowser
|
||||
import com.intellij.ui.jcef.JBCefJSQuery
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.handler.CefDisplayHandlerAdapter
|
||||
import org.cef.handler.CefLifeSpanHandlerAdapter
|
||||
import org.cef.handler.CefLoadHandlerAdapter
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Desktop
|
||||
import java.awt.CardLayout
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.AbstractAction
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTextArea
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
* 嵌入浏览器面板。
|
||||
* Harmness 嵌入浏览器面板。
|
||||
*
|
||||
* 职责:
|
||||
* - 使用 JBCefBrowser 渲染用户配置的起始地址(例如本地 Harness 地址)
|
||||
* - 工具栏:地址栏、前进/后退、刷新、设置入口(快捷跳转到 IDE Settings 的插件配置页)
|
||||
* - 通过 [JBCefJSQuery] 提供 JS ↔ 插件代码双向桥
|
||||
* - 拦截并处理 _blank 弹窗(默认吞掉,可选交给系统浏览器)
|
||||
* 四个视图(CardLayout):
|
||||
* - standby → 初始化中
|
||||
* - browser → 网页(通过 [EmbeddedBrowser] 反射包装,惰性初始化)
|
||||
* - settings → 起始地址设置表单
|
||||
* - error → 错误信息 + 一键复制
|
||||
*
|
||||
* 由于 IntelliJ Platform 官方插件模型目前不允许在 JCEF 浏览器内部直接调用
|
||||
* "打开当前项目文件" 之类的 API(无 DOM 访问权限),插件通过注入到页面中的
|
||||
* 桥接函数 [JS_BRIDGE_NAME] 让网页(如 Harness 前端)发起请求,
|
||||
* 再由本类把结果返回给页面。
|
||||
* 本类**不引用任何 JCEF 类**。浏览器能力全部走反射封装,
|
||||
* 即使 IDE 没有 JCEF,工具栏 / 设置表单 / 错误卡片也始终可用。
|
||||
*/
|
||||
class HarmnessBrowserPanel(
|
||||
private val project: com.intellij.openapi.project.Project,
|
||||
@@ -42,57 +42,58 @@ class HarmnessBrowserPanel(
|
||||
) : Disposable, JBPanel<HarmnessBrowserPanel>(BorderLayout()) {
|
||||
|
||||
private val settings = SettingsState.getInstance()
|
||||
private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
||||
|
||||
private lateinit var browser: JBCefBrowser
|
||||
private lateinit var addressField: JTextField
|
||||
private lateinit var query: JBCefJSQuery
|
||||
private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false)
|
||||
// 反射浏览器(可能为 null)
|
||||
private var browser: EmbeddedBrowser? = null
|
||||
private var bridge: EmbeddedJsBridge? = null
|
||||
|
||||
private var addressField: JTextField = JTextField()
|
||||
private var settingsUrlField: JTextField = JTextField(30)
|
||||
|
||||
private val cardLayout = CardLayout()
|
||||
private val center = JBPanel<JBPanel<*>>(cardLayout)
|
||||
private var currentCard = CARD_STANDBY
|
||||
|
||||
init {
|
||||
Disposer.register(parentDisposable, this)
|
||||
|
||||
if (!JBCefApp.isSupported()) {
|
||||
add(JBLabel(
|
||||
"JCEF (Java Chromium Embedded Framework) is not supported in this IDE instance.\n" +
|
||||
"Please use an IDE build that bundles JCEF, or a compatible JDK.", 0),
|
||||
BorderLayout.CENTER)
|
||||
return
|
||||
}
|
||||
|
||||
browser = JBCefBrowser()
|
||||
|
||||
// 布局:顶部工具栏 + 浏览器
|
||||
add(createToolbar(), BorderLayout.NORTH)
|
||||
add(browser.component, BorderLayout.CENTER)
|
||||
add(center, BorderLayout.CENTER)
|
||||
|
||||
// 设置弹窗处理
|
||||
setupPopupHandling()
|
||||
// 设置 JS 桥
|
||||
setupBridge()
|
||||
center.add(createStandbyCard(), CARD_STANDBY)
|
||||
center.add(createSettingsCard(), CARD_SETTINGS)
|
||||
center.add(createErrorCard(), CARD_ERROR)
|
||||
cardLayout.show(center, CARD_STANDBY)
|
||||
|
||||
// 初始加载
|
||||
loadCurrentUrl()
|
||||
// 窗口就绪后惰性初始化浏览器,避免阻塞 EDT
|
||||
SwingUtilities.invokeLater { initBrowserSafely() }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 工具栏
|
||||
// 工具栏(纯 Swing,不依赖 JCEF)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun createToolbar(): JComponent {
|
||||
val cefBrowser = browser.cefBrowser
|
||||
|
||||
val back = JButton(AbstractAction("←") {
|
||||
if (cefBrowser.canGoBack()) cefBrowser.goBack()
|
||||
val back = JButton(object : AbstractAction("←") {
|
||||
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goBack() } }
|
||||
})
|
||||
val forward = JButton(AbstractAction("→") {
|
||||
if (cefBrowser.canGoForward()) cefBrowser.goForward()
|
||||
val forward = JButton(object : AbstractAction("→") {
|
||||
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goForward() } }
|
||||
})
|
||||
val reload = JButton(AbstractAction("⟳") { cefBrowser.reload() })
|
||||
val settingsBtn = JButton(AbstractAction("⚙") { openSettings() })
|
||||
val externalBtn = JButton(AbstractAction("⧉") { openInExternalBrowser() })
|
||||
val reload = JButton(object : AbstractAction("⟳") {
|
||||
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.reload() } }
|
||||
})
|
||||
val settingsBtn = JButton(object : AbstractAction("⚙") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (currentCard == CARD_SETTINGS) showBrowser() else showSettings()
|
||||
}
|
||||
})
|
||||
settingsBtn.toolTipText = "设置"
|
||||
settingsBtn.border = BorderFactory.createEmptyBorder(2, 8, 2, 8)
|
||||
|
||||
addressField = JTextField(settings.startUrl)
|
||||
addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)"
|
||||
addressField.toolTipText = "当前页面地址(回车跳转)"
|
||||
addressField.addActionListener { navigate(addressField.text) }
|
||||
|
||||
val toolbar = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2))
|
||||
@@ -101,135 +102,262 @@ class HarmnessBrowserPanel(
|
||||
toolbar.add(reload)
|
||||
toolbar.add(addressField)
|
||||
toolbar.add(settingsBtn)
|
||||
toolbar.add(externalBtn)
|
||||
return toolbar
|
||||
}
|
||||
|
||||
private fun navigate(url: String) {
|
||||
val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
||||
browser.loadURL(fixedUrl)
|
||||
// ------------------------------------------------------------------
|
||||
// 视图切换
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun showCard(name: String) {
|
||||
currentCard = name
|
||||
cardLayout.show(center, name)
|
||||
}
|
||||
|
||||
private fun openSettings() {
|
||||
val app = com.intellij.openapi.application.ApplicationManager.getApplication()
|
||||
val show = Runnable {
|
||||
com.intellij.openapi.options.ShowSettingsUtil.getInstance()
|
||||
.showSettingsDialog(project, "Harmness Browser")
|
||||
}
|
||||
if (app.isDispatchThread) show.run() else app.invokeLater(show)
|
||||
private fun showSettings() {
|
||||
settingsUrlField.text = settings.startUrl
|
||||
showCard(CARD_SETTINGS)
|
||||
}
|
||||
|
||||
private fun openInExternalBrowser() {
|
||||
val url = addressField.text.ifBlank { settings.startUrl }
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(java.net.URI(url))
|
||||
} catch (e: Exception) {
|
||||
BrowserUtil.browse(url)
|
||||
}
|
||||
private fun showBrowser() {
|
||||
if (browser == null) {
|
||||
initBrowserSafely()
|
||||
} else {
|
||||
BrowserUtil.browse(url)
|
||||
showCard(CARD_BROWSER)
|
||||
loadCurrentUrl()
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 弹窗与外部链接策略
|
||||
// 浏览器初始化(反射,安全)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun setupPopupHandling() {
|
||||
browser.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() {
|
||||
override fun onBeforePopup(
|
||||
browser: CefBrowser,
|
||||
frame: org.cef.browser.CefFrame,
|
||||
targetUrl: String,
|
||||
targetFrameName: String
|
||||
): Boolean {
|
||||
// 拒绝在 IDE 内打开新窗口;改由外部系统浏览器打开
|
||||
if (targetUrl.isNotBlank()) {
|
||||
BrowserUtil.browse(targetUrl)
|
||||
}
|
||||
return true // 取消弹窗
|
||||
private fun initBrowserSafely() {
|
||||
if (browser != null) {
|
||||
showCard(CARD_BROWSER)
|
||||
loadCurrentUrl()
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!EmbeddedBrowser.isJcefSupported()) {
|
||||
showError("JCEF 不受支持",
|
||||
"当前 IDE 构建未启用 JCEF(找不到 com.intellij.ui.jcef.JBCefBrowser)。\n" +
|
||||
"请使用 JetBrains 官方发行版 IDE,或更换为内置 JCEF 的构建。")
|
||||
return
|
||||
}
|
||||
}, browser.cefBrowser)
|
||||
|
||||
browser.jbCefClient.addDisplayHandler(object : CefDisplayHandlerAdapter() {
|
||||
override fun onTitleChange(browser: CefBrowser, title: String?) {
|
||||
// 可用来同步工具窗口标题,暂时留空
|
||||
val b = EmbeddedBrowser.create()
|
||||
?: run {
|
||||
showError("无法创建浏览器",
|
||||
"反射创建 JBCefBrowser 失败。请检查 IDE 是否启用 JCEF。")
|
||||
return
|
||||
}
|
||||
browser = b
|
||||
|
||||
// JS 桥(失败不影响浏览器)
|
||||
bridge = EmbeddedJsBridge.create(b) { request ->
|
||||
logger.debug("bridge request: $request")
|
||||
try {
|
||||
val responseClass = Class.forName("com.intellij.ui.jcef.JBCefJSQuery\$Response")
|
||||
val ctor = responseClass.getConstructor(String::class.java)
|
||||
ctor.newInstance(handleBridgeRequest(request))
|
||||
} catch (_: Throwable) {
|
||||
// 无 Response 类:直接返回字符串
|
||||
handleBridgeRequest(request)
|
||||
}
|
||||
}
|
||||
}, browser.cefBrowser)
|
||||
|
||||
// 浏览器组件加入 "browser" 卡片
|
||||
val comp = b.component
|
||||
if (comp != null) {
|
||||
center.add(comp, CARD_BROWSER)
|
||||
showCard(CARD_BROWSER)
|
||||
loadCurrentUrl()
|
||||
} else {
|
||||
showError("浏览器组件不可用", "JBCefBrowser.getComponent() 返回空。")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logger.error("JCEF init failed", t)
|
||||
showError("JCEF 初始化失败", buildErrorText(t))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBridgeRequest(request: String): String = when {
|
||||
request == "ping" -> "pong"
|
||||
request == "getUrl" -> browser?.let {
|
||||
try {
|
||||
val cef = it.instance.javaClass.getMethod("getCefBrowser").invoke(it.instance)
|
||||
cef.javaClass.getMethod("getUrl").invoke(cef) as? String ?: ""
|
||||
} catch (_: Throwable) { "" }
|
||||
} ?: ""
|
||||
request.startsWith("openExternal:") -> {
|
||||
try {
|
||||
com.intellij.ide.BrowserUtil.browse(request.removePrefix("openExternal:"))
|
||||
} catch (_: Throwable) {}
|
||||
"ok"
|
||||
}
|
||||
else -> "unknown request: $request"
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// JS ↔ Java 桥
|
||||
// 错误展示(可复制)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun setupBridge() {
|
||||
query = JBCefJSQuery.create(browser)
|
||||
query.addHandler { request ->
|
||||
val r = request ?: ""
|
||||
LOG.debug("JS bridge request: $r")
|
||||
when {
|
||||
r == "ping" -> JBCefJSQuery.Response("pong")
|
||||
r == "getUrl" -> JBCefJSQuery.Response(browser.cefBrowser.url)
|
||||
r.startsWith("openExternal:") -> {
|
||||
val url = r.removePrefix("openExternal:")
|
||||
BrowserUtil.browse(url)
|
||||
JBCefJSQuery.Response("ok")
|
||||
}
|
||||
else -> JBCefJSQuery.Response("unknown request: $r", 1, "unknown")
|
||||
private fun showError(title: String, detail: String) {
|
||||
val appInfo = try {
|
||||
com.intellij.openapi.application.ApplicationInfo.getInstance().build?.asString()
|
||||
} catch (_: Throwable) { "unknown" }
|
||||
val fullText = buildString {
|
||||
appendLine("=== Harmness Browser 错误报告 ===")
|
||||
appendLine("错误: $title")
|
||||
appendLine()
|
||||
appendLine(detail)
|
||||
appendLine()
|
||||
appendLine("--- 环境信息 ---")
|
||||
appendLine("插件版本: 1.0.1")
|
||||
appendLine("IDE Build: $appInfo")
|
||||
appendLine("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}")
|
||||
appendLine("Java: ${System.getProperty("java.version")}")
|
||||
appendLine("JCEF 支持: ${EmbeddedBrowser.isJcefSupported()}")
|
||||
}
|
||||
val area = center.getClientProperty("errorArea") as? JTextArea
|
||||
if (area != null) {
|
||||
area.text = fullText
|
||||
area.caretPosition = 0
|
||||
}
|
||||
showCard(CARD_ERROR)
|
||||
}
|
||||
|
||||
private fun buildErrorText(t: Throwable): String = buildString {
|
||||
append(t.javaClass.name).append(": ").appendLine(t.message ?: "no message")
|
||||
appendLine()
|
||||
t.stackTrace.take(15).forEach { appendLine(" at $it") }
|
||||
t.cause?.let { c ->
|
||||
appendLine()
|
||||
append("Caused by: ").append(c.javaClass.name).append(": ").appendLine(c.message ?: "")
|
||||
c.stackTrace.take(8).forEach { appendLine(" at $it") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorCard(): JComponent {
|
||||
val area = JTextArea().apply {
|
||||
isEditable = false
|
||||
font = font.deriveFont(12f)
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
}
|
||||
center.putClientProperty("errorArea", area)
|
||||
|
||||
val copyBtn = JButton("📋 复制错误信息")
|
||||
copyBtn.addActionListener {
|
||||
val clip = Toolkit.getDefaultToolkit().systemClipboard
|
||||
clip.setContents(StringSelection(area.text), null)
|
||||
copyBtn.text = "✓ 已复制"
|
||||
SwingUtilities.invokeLater {
|
||||
try { Thread.sleep(1500) } catch (_: InterruptedException) {}
|
||||
copyBtn.text = "📋 复制错误信息"
|
||||
}
|
||||
}
|
||||
|
||||
// 注入桥接函数:页面加载完成后注入到 window.harmnessBridge
|
||||
browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
|
||||
override fun onLoadEnd(browser: CefBrowser, frame: org.cef.browser.CefFrame, httpStatusCode: Int) {
|
||||
if (frame.isMain && loadEnded.compareAndSet(false, true)) {
|
||||
val jsCode = """
|
||||
window.$JS_BRIDGE_NAME = function(payload) {
|
||||
var callbacks = payload && payload.callbacks;
|
||||
${query.inject(
|
||||
"payload.query",
|
||||
"function(response) { if (callbacks && callbacks.onSuccess) callbacks.onSuccess(response); }",
|
||||
"function(error_code, error_message) { if (callbacks && callbacks.onFailure) callbacks.onFailure(error_code, error_message); }"
|
||||
)};
|
||||
};
|
||||
// 通知页面桥接已就绪
|
||||
if (window.__HARMNESS_READY__) window.__HARMNESS_READY__();
|
||||
""".trimIndent()
|
||||
browser.executeJavaScript(jsCode, browser.url, 0)
|
||||
}
|
||||
}
|
||||
}, browser.cefBrowser)
|
||||
val wrapper = JBPanel<JBPanel<*>>(BorderLayout())
|
||||
wrapper.add(JBScrollPane(area), BorderLayout.CENTER)
|
||||
wrapper.add(copyBtn, BorderLayout.SOUTH)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 对外方法(供 ToolWindow 调用)
|
||||
// 其他卡片
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun createStandbyCard(): JComponent {
|
||||
return JBPanel<JBPanel<*>>(BorderLayout()).apply {
|
||||
add(JBLabel("正在初始化浏览器…", 0), BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSettingsCard(): JComponent {
|
||||
val panel = JBPanel<JBPanel<*>>().apply { layout = GridBagLayout() }
|
||||
val gbc = GridBagConstraints().apply {
|
||||
insets = Insets(8, 10, 8, 10)
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
anchor = GridBagConstraints.NORTHWEST
|
||||
}
|
||||
|
||||
panel.add(JBLabel("<html><h2 style='margin:0'>⚙ 设置</h2></html>"), gbc.also {
|
||||
it.gridx = 0; it.gridy = 0; it.gridwidth = 2
|
||||
})
|
||||
gbc.gridwidth = 1
|
||||
|
||||
panel.add(JBLabel("起始地址:"), gbc.also { it.gridx = 0; it.gridy = 1 })
|
||||
settingsUrlField = JTextField(settings.startUrl, 28)
|
||||
panel.add(settingsUrlField, gbc.also { it.gridx = 1; it.gridy = 1 })
|
||||
|
||||
panel.add(JBLabel("<html><small>例如:http://127.0.0.1:8899<br>" +
|
||||
"目标站点须允许被 iframe 嵌入(无 X-Frame-Options / CSP 限制)</small></html>"),
|
||||
gbc.also { it.gridx = 0; it.gridy = 2; it.gridwidth = 2 })
|
||||
gbc.gridwidth = 1
|
||||
|
||||
val saveBtn = JButton("保存")
|
||||
saveBtn.addActionListener {
|
||||
settings.startUrl = settingsUrlField.text.trim()
|
||||
saveBtn.text = "✓ 已保存"
|
||||
SwingUtilities.invokeLater {
|
||||
try { Thread.sleep(400) } catch (_: InterruptedException) {}
|
||||
saveBtn.text = "保存"
|
||||
}
|
||||
showBrowser()
|
||||
}
|
||||
val cancelBtn = JButton("取消")
|
||||
cancelBtn.addActionListener { showBrowser() }
|
||||
|
||||
val btnPanel = JPanel(FlowLayout(FlowLayout.LEFT, 8, 0)).apply {
|
||||
add(saveBtn); add(cancelBtn)
|
||||
}
|
||||
panel.add(btnPanel, gbc.also { it.gridx = 0; it.gridy = 3; it.gridwidth = 2 })
|
||||
|
||||
panel.add(JBPanel<JBPanel<*>>(), gbc.also {
|
||||
it.gridy = 4; it.weighty = 1.0
|
||||
})
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 浏览器操作
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun navigate(url: String) {
|
||||
val b = browser ?: return
|
||||
val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
||||
b.load(fixed)
|
||||
}
|
||||
|
||||
fun loadCurrentUrl() {
|
||||
if (!::browser.isInitialized) return
|
||||
val b = browser ?: return
|
||||
val url = settings.startUrl
|
||||
addressField.text = url
|
||||
browser.loadURL(url)
|
||||
loadEnded.set(false)
|
||||
b.load(url)
|
||||
}
|
||||
|
||||
fun reloadFromSettings() {
|
||||
loadCurrentUrl()
|
||||
fun onToolWindowSelected() {
|
||||
if (browser == null) initBrowserSafely() else loadCurrentUrl()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
override fun dispose() {
|
||||
loadEnded.set(false)
|
||||
if (::query.isInitialized) {
|
||||
query.dispose()
|
||||
}
|
||||
if (::browser.isInitialized) {
|
||||
Disposer.dispose(browser)
|
||||
}
|
||||
try { bridge?.dispose() } catch (_: Throwable) {}
|
||||
try { browser?.dispose() } catch (_: Throwable) {}
|
||||
bridge = null
|
||||
browser = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
||||
const val JS_BRIDGE_NAME = "harmnessBridge"
|
||||
private const val CARD_STANDBY = "standby"
|
||||
private const val CARD_BROWSER = "browser"
|
||||
private const val CARD_SETTINGS = "settings"
|
||||
private const val CARD_ERROR = "error"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.Configurable.WithEpName
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Settings configurable registered in plugin.xml.
|
||||
* Shows the HarmnessBrowserConfigurablePanel under Tools → Harmness Browser.
|
||||
*/
|
||||
class HarmnessSettingsConfigurable : Configurable, WithEpName {
|
||||
|
||||
private var panel: HarmnessBrowserConfigurablePanel? = null
|
||||
|
||||
override fun getDisplayName(): String = "Harmness Browser"
|
||||
|
||||
override fun getHelpTopic(): String? = null
|
||||
|
||||
override fun createComponent(): JComponent {
|
||||
panel = HarmnessBrowserConfigurablePanel()
|
||||
return panel!!.panel
|
||||
}
|
||||
|
||||
override fun isModified(): Boolean = panel?.isModified(SettingsState.getInstance()) ?: false
|
||||
|
||||
override fun apply() {
|
||||
panel?.apply(SettingsState.getInstance())
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
panel?.reset(SettingsState.getInstance())
|
||||
}
|
||||
|
||||
override fun disposeUIResources() {
|
||||
panel = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance(HarmnessSettingsConfigurable::class.java)
|
||||
}
|
||||
}
|
||||
@@ -5,63 +5,48 @@ 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.openapi.wm.ToolWindowManager
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import javax.swing.JComponent
|
||||
import com.intellij.ui.content.ContentManagerEvent
|
||||
import com.intellij.ui.content.ContentManagerListener
|
||||
import java.awt.BorderLayout
|
||||
|
||||
/**
|
||||
* 本插件的工具窗口工厂。
|
||||
* 工具窗口工厂:创建侧边栏 "Harmness Browser"。
|
||||
*
|
||||
* 工具窗口是 IntelliJ 侧边栏的区域(如 Project、Terminal、Version Control),
|
||||
* 由 IntelliJ 自动管理生命周期。我们在此创建一个自定义工具窗口 "Harmness Browser",
|
||||
* 其内容就是 [HarmnessBrowserPanel]。
|
||||
*
|
||||
* plugin.xml 中通过扩展点注册本类:
|
||||
* ```
|
||||
* <toolWindow id="Harmness Browser" ... factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory"/>
|
||||
* ```
|
||||
* 采用防御式创建:面板构造或 JCEF 初始化失败时不会让整个工具窗口空白,
|
||||
* 而是显示具体错误信息,便于排查。
|
||||
*/
|
||||
class HarmnessToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||
|
||||
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
||||
LOG.info("Creating Harmness Browser tool window for project: ${project.name}")
|
||||
|
||||
// 检查 JCEF 支持
|
||||
if (!isJcefSupported()) {
|
||||
LOG.warn("JCEF is not supported in this IDE instance. The Harmness Browser tool window will show an error message.")
|
||||
// 面板构造抛异常时,用一个错误提示面板代替,避免 IDE 显示空白内容
|
||||
val panel = try {
|
||||
HarmnessBrowserPanel(project, toolWindow.disposable)
|
||||
} catch (t: Throwable) {
|
||||
LOG.error("Failed to create HarmnessBrowserPanel", t)
|
||||
val errPanel = JBPanel<JBPanel<*>>(BorderLayout())
|
||||
val msg = buildString {
|
||||
append("创建 Harmness 面板失败:\n")
|
||||
append(t.javaClass.simpleName).append(": ").append(t.message ?: "no message")
|
||||
}
|
||||
errPanel.add(JBLabel(msg), BorderLayout.CENTER)
|
||||
errPanel
|
||||
}
|
||||
|
||||
val panel = HarmnessBrowserPanel(project, toolWindow.disposable)
|
||||
|
||||
val content = ContentFactory.getInstance().createContent(panel, "Browser", false)
|
||||
toolWindow.contentManager.addContent(content)
|
||||
|
||||
// 注册工具窗口可见性监听器,用于通知 WindowFocusService
|
||||
toolWindow.addContentManagerListener(object : com.intellij.ui.content.ContentManagerListener {
|
||||
override fun selectionChanged(selectionChange: com.intellij.ui.content.ContentManagerEvent) {
|
||||
val isActive = selectionChange.manager.selectedContent == selectionChange.content
|
||||
WindowFocusService.getInstance().setActive(isActive)
|
||||
if (isActive) {
|
||||
// 当工具窗口被选中时,触发面板刷新
|
||||
panel.loadCurrentUrl()
|
||||
}
|
||||
toolWindow.contentManager.addContentManagerListener(object : ContentManagerListener {
|
||||
override fun selectionChanged(event: ContentManagerEvent) {
|
||||
if (event.content != toolWindow.contentManager.selectedContent) return
|
||||
runCatching { (panel as? HarmnessBrowserPanel)?.onToolWindowSelected() }
|
||||
.onFailure { LOG.warn("selectionChanged handler failed", it) }
|
||||
}
|
||||
})
|
||||
|
||||
// 注册工具窗口可见性监听器(窗口获得/失去焦点)
|
||||
toolWindow.addVisibleListener { visible ->
|
||||
WindowFocusService.getInstance().setActive(visible)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isJcefSupported(): Boolean {
|
||||
return try {
|
||||
val clz = Class.forName("com.intellij.ui.jcef.JBCefApp")
|
||||
val method = clz.getMethod("isSupported")
|
||||
method.invoke(null) as Boolean
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.components.PersistentStateComponent
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* 插件设置:目前仅保存一个「浏览器起始地址」。
|
||||
* 插件设置:仅保存「浏览器起始地址」。
|
||||
*
|
||||
* 该地址必须是一个允许被嵌入的 Web 应用地址(例如本地 Harness 服务地址),
|
||||
* 并且需要在页面上开启允许被 iframe/嵌入浏览(X-Frame-Options / CSP frame-ancestors)
|
||||
* 或者由 Harness 提供专门的无 frame 限制入口。
|
||||
* 通过 @Service 注解注册(无需在 plugin.xml 中重复声明)。
|
||||
*/
|
||||
@State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")])
|
||||
@Service(Service.Level.APP)
|
||||
class SettingsState : PersistentStateComponent<SettingsState.State> {
|
||||
|
||||
data class State(
|
||||
var startUrl: String = "http://127.0.0.1:8888",
|
||||
var startUrl: String = DEFAULT_URL,
|
||||
)
|
||||
|
||||
private var myState = State()
|
||||
@@ -36,9 +37,9 @@ class SettingsState : PersistentStateComponent<SettingsState.State> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_URL = "http://127.0.0.1:8888"
|
||||
// 与 DSH Web GUI 默认端口一致
|
||||
const val DEFAULT_URL = "http://127.0.0.1:8899"
|
||||
|
||||
fun getInstance(): SettingsState =
|
||||
com.intellij.openapi.components.ServiceManager.getService(SettingsState::class.java)
|
||||
fun getInstance(): SettingsState = service()
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.harmness.toolwindow
|
||||
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* 全局单例服务,负责把「工具窗口是否可见/激活」的实时状态广播给浏览器面板。
|
||||
*
|
||||
* 之所以需要它:JCEF 浏览器的 JavaScript 侧想知道窗口何时被聚焦/失焦,
|
||||
* 以便暂停或恢复渲染(例如 Harness 界面在后台时降低 CPU 占用)。
|
||||
* 由于 ToolWindow 在内容区与焦点事件里提供的上下文有限,我们用这个轻量级
|
||||
* 事件总线在 ToolWindowManager 和 JBCefBrowser 之间传递状态。
|
||||
*/
|
||||
@Service(Service.Level.APP)
|
||||
class WindowFocusService {
|
||||
private val logger = Logger.getInstance(WindowFocusService::class.java)
|
||||
|
||||
@Volatile
|
||||
var active: Boolean = false
|
||||
private set
|
||||
|
||||
private val listeners = mutableListOf<(Boolean) -> Unit>()
|
||||
|
||||
@Synchronized
|
||||
fun addListener(listener: (Boolean) -> Unit) {
|
||||
listeners.add(listener)
|
||||
// 注册时立即补发一次当前状态,避免面板错过初始状态
|
||||
listener(active)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeListener(listener: (Boolean) -> Unit) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setActive(value: Boolean) {
|
||||
if (active == value) return
|
||||
active = value
|
||||
logger.debug("Window focus changed: $value")
|
||||
listeners.toList().forEach { it(value) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(): WindowFocusService = service()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<idea-plugin>
|
||||
<!-- This file is loaded only when com.intellij.modules.jcef plugin is present -->
|
||||
</idea-plugin>
|
||||
@@ -1,47 +1,27 @@
|
||||
<idea-plugin>
|
||||
<id>com.harmness.toolwindow</id>
|
||||
<id>com.prepeep.harmness-browser</id>
|
||||
<name>Harmness Browser</name>
|
||||
<vendor>harmness</vendor>
|
||||
<vendor>prepeep</vendor>
|
||||
|
||||
<description>
|
||||
<description><![CDATA[
|
||||
Embed a configurable browser panel (e.g. a local Harmness instance) directly inside
|
||||
JetBrains IDE sidebars. Enables seamless AI-assisted coding inside PyCharm / IntelliJ
|
||||
IDEA without switching to a separate browser window.
|
||||
</description>
|
||||
]]></description>
|
||||
|
||||
<!--
|
||||
IDE version compatibility:
|
||||
sinceBuild / untilBuild 控制插件兼容的 IDE 版本范围。
|
||||
这里要求 2024.2 及以上、2024.3.* 及以下。
|
||||
如需支持更老版本,请参考 https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||
-->
|
||||
<idea-version since-build="242" until-build="243.*"/>
|
||||
<idea-version since-build="242" until-build="263.*"/>
|
||||
|
||||
<!-- 依赖 platform -->
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<!-- 可选依赖:JCEF 存在时加载 jcef-support.xml(当前为空占位,
|
||||
未来 JCEF 相关扩展会放进该文件)。缺少 JCEF 也不影响插件加载 -->
|
||||
<depends optional="true" config-file="jcef-support.xml">com.intellij.modules.jcef</depends>
|
||||
|
||||
<!-- 工具窗口扩展点 -->
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<toolWindow
|
||||
id="Harmness Browser"
|
||||
icon="/icons/harmness.svg"
|
||||
factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory"
|
||||
anchor="right"
|
||||
condition="class#==com.intellij.ui.jcef.JBCefApp"
|
||||
/>
|
||||
|
||||
<!-- 插件设置页:Settings | Tools | Harmness Browser -->
|
||||
<applicationConfigurable
|
||||
parent="tools"
|
||||
instance="com.harmness.toolwindow.HarmnessSettingsConfigurable"
|
||||
id="com.harmness.toolwindow.HarmnessSettingsConfigurable"
|
||||
displayName="Harmness Browser"/>
|
||||
|
||||
<!-- 持久化组件 -->
|
||||
<applicationService
|
||||
serviceImplementation="com.harmness.toolwindow.SettingsState"/>
|
||||
|
||||
<applicationService
|
||||
serviceImplementation="com.harmness.toolwindow.WindowFocusService"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="rgb(96,158,255)"/>
|
||||
<stop offset="1" stop-color="rgb(150,92,255)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" rx="82" fill="url(#tile)"/>
|
||||
<circle cx="256" cy="244.74" r="137.22"
|
||||
fill="none" stroke="white" stroke-width="21.5"/>
|
||||
<ellipse cx="256" cy="244.74"
|
||||
rx="137.22" ry="57.63"
|
||||
transform="rotate(-17 256 244.74)"
|
||||
fill="none" stroke="white" stroke-width="28.16"/>
|
||||
<circle cx="349.03" cy="310.28" r="19.46" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 706 B |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="rgb(96,158,255)"/>
|
||||
<stop offset="1" stop-color="rgb(150,92,255)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" rx="81.92" fill="url(#tile)"/>
|
||||
<circle cx="256.00" cy="244.74" r="137.22"
|
||||
fill="none" stroke="white" stroke-width="21.50"/>
|
||||
<ellipse cx="256.00" cy="244.74"
|
||||
rx="137.22" ry="57.63"
|
||||
transform="rotate(-17.0 256.00 244.74)"
|
||||
fill="none" stroke="white" stroke-width="28.16"/>
|
||||
<circle cx="349.03" cy="310.28" r="19.46" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
@@ -1,12 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="rgb(96,158,255)"/>
|
||||
<stop offset="1" stop-color="rgb(150,92,255)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="40" height="40" rx="8" fill="url(#g)"/>
|
||||
<path d="M10 14h20v2H10zm0 5h20v2H10zm0 5h14v2H10z" fill="white"/>
|
||||
<circle cx="30" cy="27" r="6" fill="white" opacity=".25"/>
|
||||
<path d="M28 27l4-4" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<rect x="0" y="0" width="512" height="512" rx="82" fill="url(#tile)"/>
|
||||
<circle cx="256" cy="244.74" r="137.22"
|
||||
fill="none" stroke="white" stroke-width="21.5"/>
|
||||
<ellipse cx="256" cy="244.74"
|
||||
rx="137.22" ry="57.63"
|
||||
transform="rotate(-17 256 244.74)"
|
||||
fill="none" stroke="white" stroke-width="28.16"/>
|
||||
<circle cx="349.03" cy="310.28" r="19.46" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 706 B |
Reference in New Issue
Block a user