Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 105584f118 | |||
| 90ff9b107e | |||
| 47f3129dfb | |||
| 3fd4a01b4b | |||
| e51c046007 | |||
| b3fee364e0 | |||
| dfda6042e7 | |||
| e795320845 | |||
| c9c6744cc0 | |||
| 4890569b95 |
+10
-13
@@ -1,15 +1,12 @@
|
|||||||
# Gradle
|
.gradle
|
||||||
.gradle/
|
.idea
|
||||||
build/
|
.intellijPlatform
|
||||||
|
build
|
||||||
# IDE
|
node_modules
|
||||||
.idea/
|
web/dist
|
||||||
*.iml
|
*.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 {
|
plugins {
|
||||||
id("java")
|
id("org.jetbrains.kotlin.jvm")
|
||||||
id("org.jetbrains.kotlin.jvm") version "2.0.21"
|
id("org.jetbrains.intellij.platform")
|
||||||
id("org.jetbrains.intellij.platform") version "2.5.1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.harmness"
|
group = "com.prepeep"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -26,7 +27,7 @@ intellijPlatform {
|
|||||||
pluginConfiguration {
|
pluginConfiguration {
|
||||||
ideaVersion {
|
ideaVersion {
|
||||||
sinceBuild = "242"
|
sinceBuild = "242"
|
||||||
untilBuild = "243.*"
|
untilBuild = "263.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
|
group = com.prepeep
|
||||||
|
version = 1.0.0-SNAPSHOT
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
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
|
package com.harmness.toolwindow
|
||||||
|
|
||||||
import com.intellij.ide.BrowserUtil
|
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.util.Disposer
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.ui.components.JBLabel
|
import com.intellij.ui.components.JBLabel
|
||||||
import com.intellij.ui.components.JBPanel
|
import com.intellij.ui.components.JBPanel
|
||||||
import com.intellij.ui.jcef.JBCefApp
|
import com.intellij.ui.components.JBScrollPane
|
||||||
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 java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Desktop
|
import java.awt.CardLayout
|
||||||
import java.awt.FlowLayout
|
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.AbstractAction
|
||||||
|
import javax.swing.BorderFactory
|
||||||
import javax.swing.JButton
|
import javax.swing.JButton
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.JTextArea
|
||||||
import javax.swing.JTextField
|
import javax.swing.JTextField
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 嵌入浏览器面板。
|
* Harmness 嵌入浏览器面板。
|
||||||
*
|
*
|
||||||
* 职责:
|
* 四个视图(CardLayout):
|
||||||
* - 使用 JBCefBrowser 渲染用户配置的起始地址(例如本地 Harness 地址)
|
* - standby → 初始化中
|
||||||
* - 工具栏:地址栏、前进/后退、刷新、设置入口(快捷跳转到 IDE Settings 的插件配置页)
|
* - browser → 网页(通过 [EmbeddedBrowser] 反射包装,惰性初始化)
|
||||||
* - 通过 [JBCefJSQuery] 提供 JS ↔ 插件代码双向桥
|
* - settings → 起始地址设置表单
|
||||||
* - 拦截并处理 _blank 弹窗(默认吞掉,可选交给系统浏览器)
|
* - error → 错误信息 + 一键复制
|
||||||
*
|
*
|
||||||
* 由于 IntelliJ Platform 官方插件模型目前不允许在 JCEF 浏览器内部直接调用
|
* 本类**不引用任何 JCEF 类**。浏览器能力全部走反射封装,
|
||||||
* "打开当前项目文件" 之类的 API(无 DOM 访问权限),插件通过注入到页面中的
|
* 即使 IDE 没有 JCEF,工具栏 / 设置表单 / 错误卡片也始终可用。
|
||||||
* 桥接函数 [JS_BRIDGE_NAME] 让网页(如 Harness 前端)发起请求,
|
|
||||||
* 再由本类把结果返回给页面。
|
|
||||||
*/
|
*/
|
||||||
class HarmnessBrowserPanel(
|
class HarmnessBrowserPanel(
|
||||||
private val project: com.intellij.openapi.project.Project,
|
private val project: com.intellij.openapi.project.Project,
|
||||||
@@ -42,57 +42,58 @@ class HarmnessBrowserPanel(
|
|||||||
) : Disposable, JBPanel<HarmnessBrowserPanel>(BorderLayout()) {
|
) : Disposable, JBPanel<HarmnessBrowserPanel>(BorderLayout()) {
|
||||||
|
|
||||||
private val settings = SettingsState.getInstance()
|
private val settings = SettingsState.getInstance()
|
||||||
|
private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
||||||
|
|
||||||
private lateinit var browser: JBCefBrowser
|
// 反射浏览器(可能为 null)
|
||||||
private lateinit var addressField: JTextField
|
private var browser: EmbeddedBrowser? = null
|
||||||
private lateinit var query: JBCefJSQuery
|
private var bridge: EmbeddedJsBridge? = null
|
||||||
private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
||||||
|
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 {
|
init {
|
||||||
Disposer.register(parentDisposable, this)
|
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(createToolbar(), BorderLayout.NORTH)
|
||||||
add(browser.component, BorderLayout.CENTER)
|
add(center, BorderLayout.CENTER)
|
||||||
|
|
||||||
// 设置弹窗处理
|
center.add(createStandbyCard(), CARD_STANDBY)
|
||||||
setupPopupHandling()
|
center.add(createSettingsCard(), CARD_SETTINGS)
|
||||||
// 设置 JS 桥
|
center.add(createErrorCard(), CARD_ERROR)
|
||||||
setupBridge()
|
cardLayout.show(center, CARD_STANDBY)
|
||||||
|
|
||||||
// 初始加载
|
// 窗口就绪后惰性初始化浏览器,避免阻塞 EDT
|
||||||
loadCurrentUrl()
|
SwingUtilities.invokeLater { initBrowserSafely() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 工具栏
|
// 工具栏(纯 Swing,不依赖 JCEF)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private fun createToolbar(): JComponent {
|
private fun createToolbar(): JComponent {
|
||||||
val cefBrowser = browser.cefBrowser
|
val back = JButton(object : AbstractAction("←") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goBack() } }
|
||||||
val back = JButton(AbstractAction("←") {
|
|
||||||
if (cefBrowser.canGoBack()) cefBrowser.goBack()
|
|
||||||
})
|
})
|
||||||
val forward = JButton(AbstractAction("→") {
|
val forward = JButton(object : AbstractAction("→") {
|
||||||
if (cefBrowser.canGoForward()) cefBrowser.goForward()
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goForward() } }
|
||||||
})
|
})
|
||||||
val reload = JButton(AbstractAction("⟳") { cefBrowser.reload() })
|
val reload = JButton(object : AbstractAction("⟳") {
|
||||||
val settingsBtn = JButton(AbstractAction("⚙") { openSettings() })
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.reload() } }
|
||||||
val externalBtn = JButton(AbstractAction("⧉") { openInExternalBrowser() })
|
})
|
||||||
|
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 = JTextField(settings.startUrl)
|
||||||
addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)"
|
addressField.toolTipText = "当前页面地址(回车跳转)"
|
||||||
addressField.addActionListener { navigate(addressField.text) }
|
addressField.addActionListener { navigate(addressField.text) }
|
||||||
|
|
||||||
val toolbar = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2))
|
val toolbar = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2))
|
||||||
@@ -101,135 +102,262 @@ class HarmnessBrowserPanel(
|
|||||||
toolbar.add(reload)
|
toolbar.add(reload)
|
||||||
toolbar.add(addressField)
|
toolbar.add(addressField)
|
||||||
toolbar.add(settingsBtn)
|
toolbar.add(settingsBtn)
|
||||||
toolbar.add(externalBtn)
|
|
||||||
return toolbar
|
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() {
|
private fun showSettings() {
|
||||||
val app = com.intellij.openapi.application.ApplicationManager.getApplication()
|
settingsUrlField.text = settings.startUrl
|
||||||
val show = Runnable {
|
showCard(CARD_SETTINGS)
|
||||||
com.intellij.openapi.options.ShowSettingsUtil.getInstance()
|
|
||||||
.showSettingsDialog(project, "Harmness Browser")
|
|
||||||
}
|
|
||||||
if (app.isDispatchThread) show.run() else app.invokeLater(show)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInExternalBrowser() {
|
private fun showBrowser() {
|
||||||
val url = addressField.text.ifBlank { settings.startUrl }
|
if (browser == null) {
|
||||||
if (Desktop.isDesktopSupported()) {
|
initBrowserSafely()
|
||||||
try {
|
|
||||||
Desktop.getDesktop().browse(java.net.URI(url))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
BrowserUtil.browse(url)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
BrowserUtil.browse(url)
|
showCard(CARD_BROWSER)
|
||||||
|
loadCurrentUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 弹窗与外部链接策略
|
// 浏览器初始化(反射,安全)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private fun setupPopupHandling() {
|
private fun initBrowserSafely() {
|
||||||
browser.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() {
|
if (browser != null) {
|
||||||
override fun onBeforePopup(
|
showCard(CARD_BROWSER)
|
||||||
browser: CefBrowser,
|
loadCurrentUrl()
|
||||||
frame: org.cef.browser.CefFrame,
|
return
|
||||||
targetUrl: String,
|
}
|
||||||
targetFrameName: String
|
try {
|
||||||
): Boolean {
|
if (!EmbeddedBrowser.isJcefSupported()) {
|
||||||
// 拒绝在 IDE 内打开新窗口;改由外部系统浏览器打开
|
showError("JCEF 不受支持",
|
||||||
if (targetUrl.isNotBlank()) {
|
"当前 IDE 构建未启用 JCEF(找不到 com.intellij.ui.jcef.JBCefBrowser)。\n" +
|
||||||
BrowserUtil.browse(targetUrl)
|
"请使用 JetBrains 官方发行版 IDE,或更换为内置 JCEF 的构建。")
|
||||||
}
|
return
|
||||||
return true // 取消弹窗
|
|
||||||
}
|
}
|
||||||
}, browser.cefBrowser)
|
|
||||||
|
|
||||||
browser.jbCefClient.addDisplayHandler(object : CefDisplayHandlerAdapter() {
|
val b = EmbeddedBrowser.create()
|
||||||
override fun onTitleChange(browser: CefBrowser, title: String?) {
|
?: 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() {
|
private fun showError(title: String, detail: String) {
|
||||||
query = JBCefJSQuery.create(browser)
|
val appInfo = try {
|
||||||
query.addHandler { request ->
|
com.intellij.openapi.application.ApplicationInfo.getInstance().build?.asString()
|
||||||
val r = request ?: ""
|
} catch (_: Throwable) { "unknown" }
|
||||||
LOG.debug("JS bridge request: $r")
|
val fullText = buildString {
|
||||||
when {
|
appendLine("=== Harmness Browser 错误报告 ===")
|
||||||
r == "ping" -> JBCefJSQuery.Response("pong")
|
appendLine("错误: $title")
|
||||||
r == "getUrl" -> JBCefJSQuery.Response(browser.cefBrowser.url)
|
appendLine()
|
||||||
r.startsWith("openExternal:") -> {
|
appendLine(detail)
|
||||||
val url = r.removePrefix("openExternal:")
|
appendLine()
|
||||||
BrowserUtil.browse(url)
|
appendLine("--- 环境信息 ---")
|
||||||
JBCefJSQuery.Response("ok")
|
appendLine("插件版本: 1.0.1")
|
||||||
}
|
appendLine("IDE Build: $appInfo")
|
||||||
else -> JBCefJSQuery.Response("unknown request: $r", 1, "unknown")
|
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
|
val wrapper = JBPanel<JBPanel<*>>(BorderLayout())
|
||||||
browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
|
wrapper.add(JBScrollPane(area), BorderLayout.CENTER)
|
||||||
override fun onLoadEnd(browser: CefBrowser, frame: org.cef.browser.CefFrame, httpStatusCode: Int) {
|
wrapper.add(copyBtn, BorderLayout.SOUTH)
|
||||||
if (frame.isMain && loadEnded.compareAndSet(false, true)) {
|
return wrapper
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 对外方法(供 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() {
|
fun loadCurrentUrl() {
|
||||||
if (!::browser.isInitialized) return
|
val b = browser ?: return
|
||||||
val url = settings.startUrl
|
val url = settings.startUrl
|
||||||
addressField.text = url
|
addressField.text = url
|
||||||
browser.loadURL(url)
|
b.load(url)
|
||||||
loadEnded.set(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadFromSettings() {
|
fun onToolWindowSelected() {
|
||||||
loadCurrentUrl()
|
if (browser == null) initBrowserSafely() else loadCurrentUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 生命周期
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
loadEnded.set(false)
|
try { bridge?.dispose() } catch (_: Throwable) {}
|
||||||
if (::query.isInitialized) {
|
try { browser?.dispose() } catch (_: Throwable) {}
|
||||||
query.dispose()
|
bridge = null
|
||||||
}
|
browser = null
|
||||||
if (::browser.isInitialized) {
|
|
||||||
Disposer.dispose(browser)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
private const val CARD_STANDBY = "standby"
|
||||||
const val JS_BRIDGE_NAME = "harmnessBridge"
|
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.project.Project
|
||||||
import com.intellij.openapi.wm.ToolWindow
|
import com.intellij.openapi.wm.ToolWindow
|
||||||
import com.intellij.openapi.wm.ToolWindowFactory
|
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 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),
|
* 采用防御式创建:面板构造或 JCEF 初始化失败时不会让整个工具窗口空白,
|
||||||
* 由 IntelliJ 自动管理生命周期。我们在此创建一个自定义工具窗口 "Harmness Browser",
|
* 而是显示具体错误信息,便于排查。
|
||||||
* 其内容就是 [HarmnessBrowserPanel]。
|
|
||||||
*
|
|
||||||
* plugin.xml 中通过扩展点注册本类:
|
|
||||||
* ```
|
|
||||||
* <toolWindow id="Harmness Browser" ... factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory"/>
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
class HarmnessToolWindowFactory : ToolWindowFactory, DumbAware {
|
class HarmnessToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||||
|
|
||||||
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
||||||
LOG.info("Creating Harmness Browser tool window for project: ${project.name}")
|
LOG.info("Creating Harmness Browser tool window for project: ${project.name}")
|
||||||
|
|
||||||
// 检查 JCEF 支持
|
// 面板构造抛异常时,用一个错误提示面板代替,避免 IDE 显示空白内容
|
||||||
if (!isJcefSupported()) {
|
val panel = try {
|
||||||
LOG.warn("JCEF is not supported in this IDE instance. The Harmness Browser tool window will show an error message.")
|
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)
|
val content = ContentFactory.getInstance().createContent(panel, "Browser", false)
|
||||||
toolWindow.contentManager.addContent(content)
|
toolWindow.contentManager.addContent(content)
|
||||||
|
|
||||||
// 注册工具窗口可见性监听器,用于通知 WindowFocusService
|
toolWindow.contentManager.addContentManagerListener(object : ContentManagerListener {
|
||||||
toolWindow.addContentManagerListener(object : com.intellij.ui.content.ContentManagerListener {
|
override fun selectionChanged(event: ContentManagerEvent) {
|
||||||
override fun selectionChanged(selectionChange: com.intellij.ui.content.ContentManagerEvent) {
|
if (event.content != toolWindow.contentManager.selectedContent) return
|
||||||
val isActive = selectionChange.manager.selectedContent == selectionChange.content
|
runCatching { (panel as? HarmnessBrowserPanel)?.onToolWindowSelected() }
|
||||||
WindowFocusService.getInstance().setActive(isActive)
|
.onFailure { LOG.warn("selectionChanged handler failed", it) }
|
||||||
if (isActive) {
|
|
||||||
// 当工具窗口被选中时,触发面板刷新
|
|
||||||
panel.loadCurrentUrl()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 注册工具窗口可见性监听器(窗口获得/失去焦点)
|
|
||||||
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 {
|
companion object {
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package com.harmness.toolwindow
|
package com.harmness.toolwindow
|
||||||
|
|
||||||
import com.intellij.openapi.components.PersistentStateComponent
|
import com.intellij.openapi.components.PersistentStateComponent
|
||||||
|
import com.intellij.openapi.components.Service
|
||||||
import com.intellij.openapi.components.State
|
import com.intellij.openapi.components.State
|
||||||
import com.intellij.openapi.components.Storage
|
import com.intellij.openapi.components.Storage
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件设置:目前仅保存一个「浏览器起始地址」。
|
* 插件设置:仅保存「浏览器起始地址」。
|
||||||
*
|
*
|
||||||
* 该地址必须是一个允许被嵌入的 Web 应用地址(例如本地 Harness 服务地址),
|
* 通过 @Service 注解注册(无需在 plugin.xml 中重复声明)。
|
||||||
* 并且需要在页面上开启允许被 iframe/嵌入浏览(X-Frame-Options / CSP frame-ancestors)
|
|
||||||
* 或者由 Harness 提供专门的无 frame 限制入口。
|
|
||||||
*/
|
*/
|
||||||
@State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")])
|
@State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")])
|
||||||
|
@Service(Service.Level.APP)
|
||||||
class SettingsState : PersistentStateComponent<SettingsState.State> {
|
class SettingsState : PersistentStateComponent<SettingsState.State> {
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
var startUrl: String = "http://127.0.0.1:8888",
|
var startUrl: String = DEFAULT_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var myState = State()
|
private var myState = State()
|
||||||
@@ -36,9 +37,9 @@ class SettingsState : PersistentStateComponent<SettingsState.State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 =
|
fun getInstance(): SettingsState = service()
|
||||||
com.intellij.openapi.components.ServiceManager.getService(SettingsState::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
<idea-plugin>
|
||||||
<id>com.harmness.toolwindow</id>
|
<id>com.prepeep.harmness-browser</id>
|
||||||
<name>Harmness Browser</name>
|
<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
|
Embed a configurable browser panel (e.g. a local Harmness instance) directly inside
|
||||||
JetBrains IDE sidebars. Enables seamless AI-assisted coding inside PyCharm / IntelliJ
|
JetBrains IDE sidebars. Enables seamless AI-assisted coding inside PyCharm / IntelliJ
|
||||||
IDEA without switching to a separate browser window.
|
IDEA without switching to a separate browser window.
|
||||||
</description>
|
]]></description>
|
||||||
|
|
||||||
<!--
|
<idea-version since-build="242" until-build="263.*"/>
|
||||||
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.*"/>
|
|
||||||
|
|
||||||
<!-- 依赖 platform -->
|
|
||||||
<depends>com.intellij.modules.platform</depends>
|
<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">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
<toolWindow
|
<toolWindow
|
||||||
id="Harmness Browser"
|
id="Harmness Browser"
|
||||||
icon="/icons/harmness.svg"
|
icon="/icons/harmness.svg"
|
||||||
factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory"
|
factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory"
|
||||||
anchor="right"
|
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>
|
</extensions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M32.0845 7.94025V4H24.0203V7.9896H16.029V4H7.91553V7.94025H4V36H16.0044V32.0045C16.0058 30.9457 16.4274 29.9308 17.1766 29.1826C17.9258 28.4345 18.9412 28.0143 20 28.0143C21.0588 28.0143 22.0743 28.4345 22.8234 29.1826C23.5726 29.9308 23.9942 30.9457 23.9956 32.0045V36H36V7.94025H32.0845Z"
|
||||||
|
fill="url(#paint0_linear)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="2.94192" y1="4.89955" x2="37.7772" y2="39.7345"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.15937" stop-color="#3BEA62"/>
|
||||||
|
<stop offset="0.5404" stop-color="#3C99CC"/>
|
||||||
|
<stop offset="0.93739" stop-color="#6B57FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 818 B |
Reference in New Issue
Block a user