Initialize Harmness Browser JetBrains plugin skeleton
- Gradle + Kotlin + IntelliJ Platform SDK build - JCEF embedded browser tool window with configurable start URL - Settings page (Tools > Harmness Browser) - JS<->Java bridge scaffold (window.harmnessBridge) - Vite + React + TypeScript web scaffold for embedded page - README: build, register, upload to Marketplace, install guide
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Harmness Embedded</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "harmness-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { callPlugin, initBridge, isBridgeReady, openInExternalBrowser } from './bridge'
|
||||
|
||||
export default function App() {
|
||||
const [ready, setReady] = useState(false)
|
||||
const [pong, setPong] = useState('')
|
||||
const [status, setStatus] = useState('detecting…')
|
||||
|
||||
useEffect(() => {
|
||||
const dispose = initBridge(() => {
|
||||
setReady(true)
|
||||
setStatus('bridge ready')
|
||||
})
|
||||
return dispose
|
||||
}, [])
|
||||
|
||||
const runPing = async () => {
|
||||
try {
|
||||
setPong(await callPlugin('ping'))
|
||||
} catch (e) {
|
||||
setPong(`error: ${(e as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>Harmness Embedded</h1>
|
||||
<p className="status">
|
||||
Bridge: <b>{isBridgeReady() ? 'ready' : 'not ready'}</b> ({status})
|
||||
</p>
|
||||
|
||||
<div className="controls">
|
||||
<button onClick={runPing} disabled={!ready}>
|
||||
ping plugin
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openInExternalBrowser('https://www.jetbrains.com')}
|
||||
disabled={!ready}
|
||||
>
|
||||
open external browser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{pong && <pre className="result">{pong}</pre>}
|
||||
|
||||
<p className="hint">
|
||||
这个页面由 <code>web/</code> 下的 Vite + React + TypeScript 脚手架生成。
|
||||
开发时在 <code>web/</code> 运行 <code>npm run dev</code>(端口 8888),
|
||||
然后在插件 Settings 中把起始地址配置为 <code>http://127.0.0.1:8888</code>。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 与 JetBrains 插件(JCEF 嵌入浏览器)之间的 JS 桥接类型定义。
|
||||
*
|
||||
* 插件在页面加载完成后会向 `window` 注入全局函数 `harmnessBridge`(见
|
||||
* HarmnessBrowserPanel.kt 的 JS_BRIDGE_NAME)。网页侧通过它调用插件代码,
|
||||
* 典型用途:
|
||||
* - 打开系统浏览器访问外部链接
|
||||
* - 获取当前页面地址
|
||||
* - 未来:调用「打开编辑器」「插入文本」等 IDE 能力(需要插件侧扩展桥接协议)
|
||||
*/
|
||||
|
||||
export interface BridgeCallbacks {
|
||||
onSuccess?: (response: string) => void
|
||||
onFailure?: (errorCode: number, errorMessage: string) => void
|
||||
}
|
||||
|
||||
export interface BridgePayload {
|
||||
query: string
|
||||
callbacks?: BridgeCallbacks
|
||||
}
|
||||
|
||||
/** 插件注入的全局桥接函数签名 */
|
||||
export type HarmnessBridgeFn = (payload: BridgePayload) => void
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** 由插件注入:页面加载完成后可用 */
|
||||
harmnessBridge?: HarmnessBridgeFn
|
||||
}
|
||||
}
|
||||
|
||||
/** 单一挂载点,避免页面多次初始化时反复覆盖监听器 */
|
||||
let initialized = false
|
||||
|
||||
/** 桥接是否就绪 */
|
||||
export function isBridgeReady(): boolean {
|
||||
return typeof window.harmnessBridge === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化桥接:监听 `__HARMNESS_READY__`(插件注入桥后回调),
|
||||
* 之后即可调用 [callPlugin]。
|
||||
*/
|
||||
export function initBridge(onReady?: () => void): () => void {
|
||||
if (initialized) return () => {}
|
||||
initialized = true
|
||||
|
||||
const ready = () => {
|
||||
onReady?.()
|
||||
}
|
||||
|
||||
if (isBridgeReady()) {
|
||||
onReady?.()
|
||||
}
|
||||
|
||||
// 插件在注入桥后调用 window.__HARMNESS_READY__()
|
||||
const prev = (window as any).__HARMNESS_READY__
|
||||
;(window as any).__HARMNESS_READY__ = () => {
|
||||
if (typeof prev === 'function') prev()
|
||||
ready()
|
||||
}
|
||||
|
||||
return () => {
|
||||
initialized = false
|
||||
delete (window as any).__HARMNESS_READY__
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用插件侧方法。若桥未就绪则返回 rejected Promise。
|
||||
*
|
||||
* @param query 桥协议字符串,例如 "openExternal:https://example.com"
|
||||
* @param timeoutMs 超时(毫秒),默认 5000
|
||||
*/
|
||||
export function callPlugin(query: string, timeoutMs = 5000): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const bridge = window.harmnessBridge
|
||||
if (typeof bridge !== 'function') {
|
||||
reject(new Error('harmnessBridge is not available yet'))
|
||||
return
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
reject(new Error(`Bridge call timed out after ${timeoutMs}ms: ${query}`))
|
||||
}, timeoutMs)
|
||||
|
||||
bridge({
|
||||
query,
|
||||
callbacks: {
|
||||
onSuccess: (response) => {
|
||||
window.clearTimeout(timer)
|
||||
resolve(response)
|
||||
},
|
||||
onFailure: (code, message) => {
|
||||
window.clearTimeout(timer)
|
||||
reject(new Error(`Bridge error ${code}: ${message}`))
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 便捷方法
|
||||
export const openInExternalBrowser = (url: string) => callPlugin(`openExternal:${url}`)
|
||||
export const ping = () => callPlugin('ping')
|
||||
@@ -0,0 +1,67 @@
|
||||
:root {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
color: #1f2328;
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #57606a;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
background: #0d1117;
|
||||
color: #7ee787;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 13px;
|
||||
color: #57606a;
|
||||
border-top: 1px solid #d8dee4;
|
||||
padding-top: 12px;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
// 默认端口,与插件 Settings 默认地址一致
|
||||
port: 8888,
|
||||
strictPort: false,
|
||||
},
|
||||
build: {
|
||||
// 输出到插件 resources 目录(可选:也可用 Vite dev server 做开发)
|
||||
outDir: 'dist',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user