RingUI + JCEF开发IDEA插件

文章目录 RingUI知识储备示例插件实现逻辑开发环境开发流程新建一个IDEA插件项目新建一个前端项目验证前端项目丰富前端项目丰富插件内容

RingUI

This collection of UI components aims to provide all the necessary building blocks for web-based products built inside JetBrains, as well as third-party plugins developed for JetBrains’ products.

JetBrains团队发布了一套基于Web的、和JetBrains产品风格相同的UI组件库ring-ui,我觉得这个UI组件库有以下使用场景:

开发一个JetBrains风格的网页应用,比如开发了一个JetBrains插件,提供一个网站来介绍该插件;开发一个基于Web的JetBrains插件,比如Markdown预览插件,这类插件如果使用Java Swing来实现,基本不可能,但是直接使用web组件却很方便;开发一个JetBrains插件,但是界面非常复杂,开发人员对Java Swing不熟悉,但对react和web ui组件非常熟悉,可直接使用ring-ui来开发用户界面。 知识储备

RingUI:ring-ui

RingUI文档:ring-ui 文档

CEF(Chromium Embedded Framework)/JCEF(Java Chromium Embedded Framework)/JBCEF(JetBrains Chromium Embedded Framework):

Chromium Embedded Framework: 基于Google Chromium项目的开源Web browser控件,C++实现,支持Windows, Linux, Mac平台,即可以在自己的C++项目中嵌入一个Chrome内核的浏览器,并且支持各种自定义接口,比如自定义响应下载、自定义拦截请求、自定义浏览器控制台等,类似一个非常强大的WebView; Java Chromium Embedded Framework: 通过JNI机制,将C++的接口通过Java暴露,并且支持将浏览器嵌入JFrame。 JetBrains Chromium Embedded Framework: JetBrains在JCEF的基础上进一步作出的封装JetBrains/jcef IDEA 2020.1版本中,JCEF作为一个试验性功能,需要通过一些额外操作才能启用JCEF;在IDEA 2020.2+版本中,JCEF默认集成并启用,成为一个标准功能:JCEF – Java Chromium Embedded Framework

React: reactjs

RingUI是一个React组件库,需要对React有一定的了解

JOOQ SQL Translation:SQL Translation

这是一个在线的sql方言翻译工具,可以把一种数据库的SQL语法转换成另一种数据的SQL语法,比如Oracle转成MySQL

示例插件

希望有一个IDEA插件,点击后打开一个弹窗,展示类似于百度翻译的界面,选择源数据库和目标数据库,点击翻译能完成SQL语法的转换,有如下限制和要求:

JOOQ有开源版本,并且暴露了SQL转换的方法,但不能用开源版本的JOOQ实现,必须访问https://www.jooq.org/translate/translate接口来实现转换,因为开源版本的JOOQ只支持开源数据库之间的SQL转换,商业数据库需要购买license,而在线的SQL转换支持所有JOOQ支持的数据库,包含Oralce在内的商业数据库;不允许直接嵌入浏览器,直接访问https://www.jooq.org/translate/,因为默认的界面太丑,而且有很多与转换无关的信息;界面的风格需要跟随IDEA本身风格,即跟随IDEA的深色和浅色模式; 实现逻辑 界面由单独的React + RingUI前端项目实现,npm run build构建后将build目录的内容拷贝至插件resource目录下;在IDEA顶部tools菜单下添加一个SQL Translator菜单,点击后弹出一个窗口,使用JCEF渲染resource目录下的index.html,JCEF访问一个虚拟地址,为JCEF添加一个请求资源拦截器,根据访问的URI决定返回resource目录下的静态资源,还是调用远程接口;风格切换实现逻辑:JCEF访问index.html之前,先获取当前IDEA的风格,然后在URL后面加上Query参数,如index.html?theme=dark,React解析参数,动态设定页面风格。 开发环境 IDEA:IntelliJ IDEA 2022.3 (Ultimate Edition)Gradle:7.5.1JDK: 11.0.11React:18.2.0node/npm: v16.18.1/8.19.2 开发流程 新建一个IDEA插件项目

假设项目名称为sql-translator

创建完成后,系统会自动下载Gradle、IDEA SDK、JBCEF(JetBrains CEF)等,下载时间可能比较长。

新建一个前端项目

假设项目名称为react-client

# 安装必要依赖npm install –save react react-dom webpack webpack-dev-server html-webpack-pluginnpm install –save –dev babel-loader babel-core babel-preset-react babel-preset-env@next# 安装create-react-appnpm install -g create-react-app# 切换到插件项目根目录cd sql-translator# 新建项目create-react-app react-client# 切换到前端项目根目录cd react-client# 安装ring-uinpm install @jetbrains/ring-ui 验证前端项目

替换前端项目src/App.js内容为

import ‘@jetbrains/ring-ui/dist/style.css’;import alertService from ‘@jetbrains/ring-ui/dist/alert-service/alert-service’;import Button from ‘@jetbrains/ring-ui/dist/button/button’;function App() { return ( “start”: “react-scripts start”, “build”: “react-scripts build && rm -rf ../src/main/resources/html/* && cp -r build/* ../src/main/resources/html/”, “test”: “react-scripts test”, “eject”: “react-scripts eject” }

App.js

import ‘./App.css’;import ‘@jetbrains/ring-ui/dist/style.css’;import React, {Component} from ‘react’import Button from ‘@jetbrains/ring-ui/dist/button/button’;import compareIcon from ‘@jetbrains/icons/compare’;import Select from ‘@jetbrains/ring-ui/dist/select/select’;import Icon from ‘@jetbrains/ring-ui/dist/icon/icon’;import Input, {Size} from ‘@jetbrains/ring-ui/dist/input/input’;import Checkbox from ‘@jetbrains/ring-ui/dist/checkbox/checkbox’;import Theme, {ThemeProvider, ThemedWrapper} from ‘@jetbrains/ring-ui/dist/global/theme’;import CodeMirror from ‘@uiw/react-codemirror’;import {sql} from ‘@codemirror/lang-sql’;import axios from “axios”export default class App extends Component { constructor(props) { super(props); this.changeHandle = this.changeHandle.bind(this); this.selectHandle = this.selectHandle.bind(this); const params = new URLSearchParams(window.location.search); this.state = { “from-dialect”: “DEFAULT”, “from-search-path”: “PUBLIC”, “from-unknown-functions”: false, “from-date-format”: “YYYY-MM-DD”, “from-timestamp-format”: “YYYY-MM-DD HH24:MI:SS.FF”, “from-retain-comments-between-queries”: false, “from-ignore-comments”: true, “from-ignore-comment-start”: “[jooq ignore start]”, “from-ignore-comment-stop”: “[jooq ignore stop]”, “to-dialect”: “DEFAULT”, “to-keywords”: “LOWER”, “to-name-case”: “AS_IS”, “to-name-quoted”: “EXPLICIT_DEFAULT_QUOTED”, “to-param-type”: “NAMED”, “to-patterns”: “OFF”, “to-join-style”: “DEFAULT”, “to-qualify”: “WHEN_NEEDED”, “to-rownum”: “WHEN_NEEDED”, “to-inline-cte”: “WHEN_NEEDED”, “to-group-by-column-index”: “WHEN_NEEDED”, “to-unnecessary-arithmetic”: “INTERNAL”, “to-field-as”: “DEFAULT”, “to-table-as”: “DEFAULT”, “to-inner-keyword”: “DEFAULT”, “to-outer-keyword”: “DEFAULT”, sql: “”, translateResult: “”, loading: false, theme: params.get(“theme”) } } fromDialect = [ {label: ‘No specific dialect’, key: ‘DEFAULT’, type: ‘from-dialect’}, {label: ‘BigQuery’, key: ‘BIGQUERY’, type: ‘from-dialect’}, {label: ‘CockroachDB’, key: ‘COCKROACHDB’, type: ‘from-dialect’}, {label: ‘DB2 LUW’, key: ‘DB2’, type: ‘from-dialect’}, {label: ‘Derby’, key: ‘DERBY’, type: ‘from-dialect’}, {label: ‘Exasol’, key: ‘EXASOL’, type: ‘from-dialect’}, {label: ‘Firebird’, key: ‘FIREBIRD’, type: ‘from-dialect’}, {label: ‘H2’, key: ‘H2’, type: ‘from-dialect’}, {label: ‘HANA’, key: ‘HANA’, type: ‘from-dialect’}, {label: ‘HSQLDB’, key: ‘HSQLDB’, type: ‘from-dialect’}, {label: ‘Ignite’, key: ‘IGNITE’, type: ‘from-dialect’}, {label: ‘Informix’, key: ‘INFORMIX’, type: ‘from-dialect’}, {label: ‘Ingres’, key: ‘INGRES’, type: ‘from-dialect’}, {label: ‘MariaDB’, key: ‘MARIADB’, type: ‘from-dialect’}, {label: ‘MemSQL (SingleStore)’, key: ‘MEMSQL’, type: ‘from-dialect’}, {label: ‘MySQL’, key: ‘MYSQL’, type: ‘from-dialect’}, {label: ‘MS Access’, key: ‘ACCESS’, type: ‘from-dialect’}, {label: ‘Oracle’, key: ‘ORACLE’, type: ‘from-dialect’}, {label: ‘PostgreSQL’, key: ‘POSTGRES’, type: ‘from-dialect’}, {label: ‘Redshift’, key: ‘REDSHIFT’, type: ‘from-dialect’}, {label: ‘Snowflake’, key: ‘SNOWFLAKE’, type: ‘from-dialect’}, {label: ‘SQL Data Warehouse (Azure Synapse Analytics)’, key: ‘SQLDATAWAREHOUSE’, type: ‘from-dialect’}, {label: ‘SQLite’, key: ‘SQLITE’, type: ‘from-dialect’}, {label: ‘SQL Server’, key: ‘SQLSERVER’, type: ‘from-dialect’}, {label: ‘Sybase ASE’, key: ‘ASE’, type: ‘from-dialect’}, {label: ‘Sybase SQL Anywhere’, key: ‘SYBASE’, type: ‘from-dialect’}, {label: ‘Teradata’, key: ‘TERADATA’, type: ‘from-dialect’}, {label: ‘Vertica’, key: ‘VERTICA’, type: ‘from-dialect’}, {label: ‘YugabyteDB’, key: ‘YUGABYTEDB’, type: ‘from-dialect’}, ]; toDialect = [ {label: ‘No specific dialect’, key: ‘DEFAULT’, type: ‘to-dialect’}, {label: ‘Aurora MySQL’, key: ‘AURORA_MYSQL’, type: ‘to-dialect’}, {label: ‘Aurora PostgreSQL’, key: ‘AURORA_POSTGRES’, type: ‘to-dialect’}, {label: ‘BigQuery’, key: ‘BIGQUERY’, type: ‘to-dialect’}, {label: ‘CockroachDB’, key: ‘COCKROACHDB’, type: ‘to-dialect’}, {label: ‘DB2 LUW 9’, key: ‘DB2_9’, type: ‘to-dialect’}, {label: ‘DB2 LUW 10’, key: ‘DB2_10’, type: ‘to-dialect’}, {label: ‘DB2 LUW 11’, key: ‘DB2_11’, type: ‘to-dialect’}, {label: ‘DB2 LUW (latest version)’, key: ‘DB2’, type: ‘to-dialect’}, {label: ‘Derby’, key: ‘DERBY’, type: ‘to-dialect’}, {label: ‘Exasol’, key: ‘EXASOL’, type: ‘to-dialect’}, {label: ‘Firebird 2.5’, key: ‘FIREBIRD_2_5’, type: ‘to-dialect’}, {label: ‘Firebird 3.0’, key: ‘FIREBIRD_3_0’, type: ‘to-dialect’}, {label: ‘Firebird 4.0’, key: ‘FIREBIRD_4_0’, type: ‘to-dialect’}, {label: ‘Firebird (latest version)’, key: ‘FIREBIRD’, type: ‘to-dialect’}, {label: ‘H2 1.4’, key: ‘H2_1_4_200’, type: ‘to-dialect’}, {label: ‘H2 2.0’, key: ‘H2_2_0_202’, type: ‘to-dialect’}, {label: ‘H2 (latest version)’, key: ‘H2’, type: ‘to-dialect’}, {label: ‘HANA’, key: ‘HANA’, type: ‘to-dialect’}, {label: ‘HSQLDB’, key: ‘HSQLDB’, type: ‘to-dialect’}, {label: ‘Ignite’, key: ‘IGNITE’, type: ‘to-dialect’}, {label: ‘Informix’, key: ‘INFORMIX’, type: ‘to-dialect’}, {label: ‘Ingres’, key: ‘INGRES’, type: ‘to-dialect’}, {label: ‘MariaDB 10.0’, key: ‘MARIADB_10_0’, type: ‘to-dialect’}, {label: ‘MariaDB 10.1’, key: ‘MARIADB_10_1’, type: ‘to-dialect’}, {label: ‘MariaDB 10.2’, key: ‘MARIADB_10_2’, type: ‘to-dialect’}, {label: ‘MariaDB 10.3’, key: ‘MARIADB_10_3’, type: ‘to-dialect’}, {label: ‘MariaDB 10.4’, key: ‘MARIADB_10_4’, type: ‘to-dialect’}, {label: ‘MariaDB 10.5’, key: ‘MARIADB_10_5’, type: ‘to-dialect’}, {label: ‘MariaDB 10.6’, key: ‘MARIADB_10_6’, type: ‘to-dialect’}, {label: ‘MariaDB (latest version)’, key: ‘MARIADB’, type: ‘to-dialect’}, {label: ‘MemSQL (SingleStore)’, key: ‘MEMSQL’, type: ‘to-dialect’}, {label: ‘MySQL 5.7’, key: ‘MYSQL_5_7’, type: ‘to-dialect’}, {label: ‘MySQL 8.0’, key: ‘MYSQL_8_0’, type: ‘to-dialect’}, {label: ‘MySQL 8.0.19’, key: ‘MYSQL_8_0_19’, type: ‘to-dialect’}, {label: ‘MySQL (latest version)’, key: ‘MYSQL’, type: ‘to-dialect’}, {label: ‘MS Access’, key: ‘ACCESS’, type: ‘to-dialect’}, {label: ‘Oracle 10g’, key: ‘ORACLE10G’, type: ‘to-dialect’}, {label: ‘Oracle 11g’, key: ‘ORACLE11G’, type: ‘to-dialect’}, {label: ‘Oracle 12c’, key: ‘ORACLE12C’, type: ‘to-dialect’}, {label: ‘Oracle 18c’, key: ‘ORACLE18C’, type: ‘to-dialect’}, {label: ‘Oracle 20c’, key: ‘ORACLE20C’, type: ‘to-dialect’}, {label: ‘Oracle (latest version)’, key: ‘ORACLE’, type: ‘to-dialect’}, {label: ‘PostgreSQL 9.3’, key: ‘POSTGRES_9_3’, type: ‘to-dialect’}, {label: ‘PostgreSQL 9.4’, key: ‘POSTGRES_9_4’, type: ‘to-dialect’}, {label: ‘PostgreSQL 9.5’, key: ‘POSTGRES_9_5’, type: ‘to-dialect’}, {label: ‘PostgreSQL 10’, key: ‘POSTGRES_10’, type: ‘to-dialect’}, {label: ‘PostgreSQL 11’, key: ‘POSTGRES_11’, type: ‘to-dialect’}, {label: ‘PostgreSQL 12’, key: ‘POSTGRES_12’, type: ‘to-dialect’}, {label: ‘PostgreSQL 13’, key: ‘POSTGRES_13’, type: ‘to-dialect’}, {label: ‘PostgreSQL 14’, key: ‘POSTGRES_14’, type: ‘to-dialect’}, {label: ‘PostgreSQL 15’, key: ‘POSTGRES_15’, type: ‘to-dialect’}, {label: ‘PostgreSQL (latest version)’, key: ‘POSTGRES’, type: ‘to-dialect’}, {label: ‘Redshift’, key: ‘REDSHIFT’, type: ‘to-dialect’}, {label: ‘Snowflake’, key: ‘SNOWFLAKE’, type: ‘to-dialect’}, {label: ‘SQL Data Warehouse (Azure Synapse Analytics)’, key: ‘SQLDATAWAREHOUSE’, type: ‘to-dialect’}, {label: ‘SQLite 3.25’, key: ‘SQLITE_3_25’, type: ‘to-dialect’}, {label: ‘SQLite 3.28’, key: ‘SQLITE_3_28’, type: ‘to-dialect’}, {label: ‘SQLite 3.30’, key: ‘SQLITE_3_30’, type: ‘to-dialect’}, {label: ‘SQLite (latest version)’, key: ‘SQLITE’, type: ‘to-dialect’}, {label: ‘SQL Server 2008’, key: ‘SQLSERVER2008’, type: ‘to-dialect’}, {label: ‘SQL Server 2012’, key: ‘SQLSERVER2012’, type: ‘to-dialect’}, {label: ‘SQL Server 2014’, key: ‘SQLSERVER2014’, type: ‘to-dialect’}, {label: ‘SQL Server 2016’, key: ‘SQLSERVER2016’, type: ‘to-dialect’}, {label: ‘SQL Server 2017’, key: ‘SQLSERVER2017’, type: ‘to-dialect’}, {label: ‘SQL Server 2022’, key: ‘SQLSERVER2022’, type: ‘to-dialect’}, {label: ‘SQL Server (latest version)’, key: ‘SQLSERVER’, type: ‘to-dialect’}, {label: ‘Sybase ASE 12.5’, key: ‘ASE_12_5’, type: ‘to-dialect’}, {label: ‘Sybase ASE 15.5’, key: ‘ASE_15_5’, type: ‘to-dialect’}, {label: ‘Sybase ASE 15.7’, key: ‘ASE_15_7’, type: ‘to-dialect’}, {label: ‘Sybase ASE 16.0’, key: ‘ASE_16_0’, type: ‘to-dialect’}, {label: ‘Sybase ASE (latest version)’, key: ‘ASE’, type: ‘to-dialect’}, {label: ‘Sybase SQL Anywhere’, key: ‘SYBASE’, type: ‘to-dialect’}, {label: ‘Teradata’, key: ‘TERADATA’, type: ‘to-dialect’}, {label: ‘Vertica’, key: ‘VERTICA’, type: ‘to-dialect’}, {label: ‘YugabyteDB’, key: ‘YUGABYTEDB’, type: ‘to-dialect’}, {label: ‘jOOQ (Java)’, key: ‘JAVA’, type: ‘to-dialect’}, ]; toKeywords = [ {label: ‘lower case’, key: ‘LOWER’, type: ‘to-keywords’}, {label: ‘UPPER CASE’, key: ‘UPPER’, type: ‘to-keywords’}, {label: ‘Pascal Case’, key: ‘PASCAL’, type: ‘to-keywords’}, ]; identifierCase = [ {label: ‘Unmodified’, key: ‘AS_IS’, type: ‘to-name-case’}, {label: ‘lower case’, key: ‘LOWER’, type: ‘to-name-case’}, {label: ‘lower case (if unquoted)’, key: ‘LOWER_IF_UNQUOTED’, type: ‘to-name-case’}, {label: ‘UPPER’, key: ‘UPPER CASE’, type: ‘to-name-case’}, {label: ‘UPPER CASE (if unquoted)’, key: ‘UPPER_IF_UNQUOTED’, type: ‘to-name-case’}, ]; identifierQuoting = [ {label: ‘Always’, key: ‘ALWAYS’, type: ‘to-name-quoted’}, {label: ‘Unmodified (default quoted)’, key: ‘EXPLICIT_DEFAULT_QUOTED’, type: ‘to-name-quoted’}, {label: ‘Unmodified (default unquoted)’, key: ‘EXPLICIT_DEFAULT_UNQUOTED’, type: ‘to-name-quoted’}, {label: ‘Never’, key: ‘NEVER’, type: ‘to-name-quoted’}, ]; bindVariables = [ {label: ‘Named’, key: ‘NAMED’, type: ‘to-param-type’}, {label: ‘Indexed’, key: ‘INDEXED’, type: ‘to-param-type’}, {label: ‘Force Indexed’, key: ‘FORCE_INDEXED’, type: ‘to-param-type’}, ]; toPatterns = [ {label: ‘Unmodified’, key: ‘OFF’, type: ‘to-patterns’}, {label: ‘Transform patterns’, key: ‘ON’, type: ‘to-patterns’}, ]; joinStyle = [ {label: ‘Unmodified’, key: ‘DEFAULT’, type: ‘to-join-style’}, {label: ‘Oracle style to ANSI join’, key: ‘ANSI’, type: ‘to-join-style’}, {label: ‘ANSI join to Oracle style’, key: ‘ORACLE’, type: ‘to-join-style’}, ]; qualify = [ {label: ‘Transform QUALIFY’, key: ‘ALWAYS’, type: ‘to-qualify’}, {label: ‘Transform QUALIFY when not supported’, key: ‘WHEN_NEEDED’, type: ‘to-qualify’}, {label: “Don’t transform QUALIFY”, key: ‘NEVER’, type: ‘to-qualify’}, ]; rownum = [ {label: ‘Transform ROWNUM’, key: ‘ALWAYS’, type: ‘to-rownum’}, {label: ‘Transform ROWNUM when not supported’, key: ‘WHEN_NEEDED’, type: ‘to-rownum’}, {label: “Don’t transform ROWNUM”, key: ‘NEVER’, type: ‘to-rownum’}, ]; inlineCte = [ {label: ‘Transform inline CTE’, key: ‘ALWAYS’, type: ‘to-inline-cte’}, {label: ‘Transform inline CTE when not supported’, key: ‘WHEN_NEEDED’, type: ‘to-inline-cte’}, {label: “Don’t transform inline CTE”, key: ‘NEVER’, type: ‘to-inline-cte’}, ]; groupByColumnIndex = [ {label: ‘Transform GROUP BY ‘, key: ‘ALWAYS’, type: ‘to-group-by-column-index’}, {label: ‘Transform GROUP BY when not supported’, key: ‘WHEN_NEEDED’, type: ‘to-group-by-column-index’}, {label: “Don’t transform GROUP BY “, key: ‘NEVER’, type: ‘to-group-by-column-index’}, ]; unnecessaryArithmetic = [ {label: ‘Internal (from emulations)’, key: ‘INTERNAL’, type: ‘to-unnecessary-arithmetic’}, {label: ‘Never’, key: ‘NEVER’, type: ‘to-unnecessary-arithmetic’}, {label: “Always”, key: ‘ALWAYS’, type: ‘to-unnecessary-arithmetic’}, ]; toFieldAs = [ {label: ‘Default’, key: ‘DEFAULT’, type: ‘to-field-as’}, {label: ‘Off’, key: ‘OFF’, type: ‘to-field-as’}, {label: “On”, key: ‘ON’, type: ‘to-field-as’}, ]; toTableAs = [ {label: ‘Default’, key: ‘DEFAULT’, type: ‘to-table-as’}, {label: ‘Off’, key: ‘OFF’, type: ‘to-table-as’}, {label: “On”, key: ‘ON’, type: ‘to-table-as’}, ]; toInnerKeyword = [ {label: ‘Default’, key: ‘DEFAULT’, type: ‘to-inner-keyword’}, {label: ‘Off’, key: ‘OFF’, type: ‘to-inner-keyword’}, {label: “On”, key: ‘ON’, type: ‘to-inner-keyword’}, ]; toOuterKeyword = [ {label: ‘Default’, key: ‘DEFAULT’, type: ‘to-outer-keyword’}, {label: ‘Off’, key: ‘OFF’, type: ‘to-outer-keyword’}, {label: “On”, key: ‘ON’, type: ‘to-outer-keyword’}, ]; setStateValue = (e) => { const targetName = e.type; const targetValue = e.key; this.setState({[targetName]: targetValue}, () => { console.log(this.state[targetName]) }); console.log(this.state); } translate = () => { this.setState({loading: true}, () => { const header = {headers: {‘Content-Type’: ‘application/x-www-form-urlencoded;charset=UTF-8’, ‘Access-Control-Allow-Origin’: ‘*’}}; axios.post(“translate”, { …this.state }, header).then((res) => { console.log(res) this.setState({ translateResult: res.data, loading: false }) }).catch((reason) => { this.setState({ translateResult: reason.message, loading: false }) }); }); } changeHandle(e) { e.preventDefault() let key = e.target.dataset.key; this.setState({[key]: e.target.value}, () => { console.log(this.state[key]) }) } selectHandle(e) { console.log(e) let key = e.target.dataset.key; this.setState({[key]: e.target.checked}, () => { console.log(this.state[key]) }) } render() { const snapshot = this.state; const theme = this.state.theme && this.state.theme === ‘dark’ ? “dark” : “light”; const jbTheme = this.state.theme && this.state.theme === ‘dark’ ? Theme.DARK : Theme.LIGHT; return {padding: “10px”}} className=”themed-wrapper”> true} label={“—“} selected={this.fromDialect.filter((i) => i.key === snapshot[‘from-dialect’])} onChange={(e) => this.setStateValue(e)} data={this.fromDialect}> “gap-large”} color={Icon.Color.BLUE}/> true} label={“—“} selected={this.toDialect.filter((i) => i.key === snapshot[‘to-dialect’])} onChange={(e) => this.setStateValue(e)} data={this.toDialect}> this.state.loading}> Translate marginLeft: “5px”, display: this.state.loading ? “inline-block” : “none”}}/> display: “flex”}}> flex: “1”, marginRight: “5px”, marginTop: “15px”}}> theme} width=”calc(100vw/2 – 25px)” onChange={(value, view) => { this.setState({sql: value}) }} /> Size.FULL} data-key=”from-search-path” label=”Search path, comma separated” onChange={this.changeHandle} value={snapshot[“from-search-path”]}/> snapshot[“from-unknown-functions”]}/> this.changeHandle} value={snapshot[“from-date-format”]}/> this.changeHandle} value={snapshot[“from-timestamp-format”]}/> snapshot[“from-retain-comments-between-queries”]}/> snapshot[“from-ignore-comments”]}/> this.changeHandle} value={snapshot[“from-ignore-comment-start”]}/> this.changeHandle} value={snapshot[“from-ignore-comment-stop”]}/> flex: “1”, marginLeft: “5px”, marginTop: “15px”}}> [sql()]} theme={theme} /> Size.FULL} selectedLabel=”Keywords” selected={this.toKeywords.filter((i) => i.key === this.state[‘to-keywords’])} onChange={(e) => this.setStateValue(e)} data={this.toKeywords}> this.identifierCase.filter((i) => i.key === this.state[‘to-name-case’])} onChange={(e) => this.setStateValue(e)} data={this.identifierCase}> this.identifierQuoting.filter((i) => i.key === this.state[‘to-name-quoted’])} onChange={(e) => this.setStateValue(e)} data={this.identifierQuoting}> this.bindVariables.filter((i) => i.key === this.state[‘to-param-type’])} onChange={(e) => this.setStateValue(e)} data={this.bindVariables}> this.toPatterns.filter((i) => i.key === this.state[‘to-patterns’])} onChange={(e) => this.setStateValue(e)} data={this.toPatterns}> this.joinStyle.filter((i) => i.key === this.state[‘to-join-style’])} onChange={(e) => this.setStateValue(e)} data={this.joinStyle}> this.qualify.filter((i) => i.key === this.state[‘to-qualify’])} onChange={(e) => this.setStateValue(e)} data={this.qualify}> this.rownum.filter((i) => i.key === this.state[‘to-rownum’])} onChange={(e) => this.setStateValue(e)} data={this.rownum}> this.inlineCte.filter((i) => i.key === this.state[‘to-inline-cte’])} onChange={(e) => this.setStateValue(e)} data={this.inlineCte}> this.groupByColumnIndex.filter((i) => i.key === this.state[‘to-group-by-column-index’])} onChange={(e) => this.setStateValue(e)} data={this.groupByColumnIndex}> this.unnecessaryArithmetic.filter((i) => i.key === this.state[‘to-unnecessary-arithmetic’])} onChange={(e) => this.setStateValue(e)} data={this.unnecessaryArithmetic}> this.toFieldAs.filter((i) => i.key === this.state[‘to-field-as’])} onChange={(e) => this.setStateValue(e)} data={this.toFieldAs}> this.toTableAs.filter((i) => i.key === this.state[‘to-table-as’])} onChange={(e) => this.setStateValue(e)} data={this.toTableAs}> this.toInnerKeyword.filter((i) => i.key === this.state[‘to-inner-keyword’])} onChange={(e) => this.setStateValue(e)} data={this.toInnerKeyword}> this.toOuterKeyword.filter((i) => i.key === this.state[‘to-outer-keyword’])} onChange={(e) => this.setStateValue(e)} data={this.toOuterKeyword}> } ;}

App.css

.App { text-align: center;}.gap-right { margin-right: 5px;}.gap-large { margin-right: 10px; margin-left: 5px;}.themed-wrapper { border: solid 1px var(–ring-borders-color); border-radius: var(–ring-border-radius); background-color: var(–ring-content-background-color); padding: var(–ring-unit);}.themed-wrapper-with-top-gap { border: solid 1px var(–ring-borders-color); border-radius: var(–ring-border-radius); background-color: var(–ring-content-background-color); margin-top: var(–ring-unit); padding: var(–ring-unit);}

前端主要逻辑为:

复刻JOOQ SQL Translate页面,左右两侧的SQL输入和展示组件用codemirror,实现SQL代码高亮和提示点击翻译按钮,用axios发送/translate请求,参数和JOOQ SQL Translate一样请求成功后,将返回值填充到右侧代码展示区

npm start后可看到页面基本和JOOQ SQL Translate页面相差无几,在浏览器地址栏后添加?theme=dark或?theme=light可看到风格切换。 npm run build后,插件的resource/html目录下会生成用户界面静态资源。

丰富插件内容

在build.gradle.kts添加okhttp3依赖:

dependencies { implementation(“com.squareup.okhttp3:okhttp:4.10.0”)}

新建com.github.clyoudu.sqltrans.frame包,在该包下创建TranslatorFrame.java:

package com.github.clyoudu.sqltrans.frame;import java.awt.*;import javax.swing.*;import com.intellij.ui.jcef.JBCefBrowser;import com.intellij.ui.jcef.JBCefClient;import com.intellij.util.ui.UIUtil;import com.github.clyoudu.sqltrans.jcefhandler.LocalRequestHandler;/** * TranslatorFrame. * * @author leichen * @since 1.0, 2022/12/27 5:28 PM */public class TranslatorFrame extends JFrame { private JBCefBrowser jbCefBrowser; private JBCefClient jbCefClient; private final JPanel dialogPanel; private TranslatorFrame() { super(“SQLTranslator”); dialogPanel = new JPanel(new BorderLayout()); add(dialogPanel); } @Override public void setVisible(boolean b) { if (b) { setSize(1024, 750); setLocationRelativeTo(null); jbCefBrowser = new JBCefBrowser(); jbCefClient = jbCefBrowser.getJBCefClient(); jbCefClient.addRequestHandler(new LocalRequestHandler(), jbCefBrowser.getCefBrowser()); // 将 JBCefBrowser 的UI控件设置到Panel中 dialogPanel.add(jbCefBrowser.getComponent(), BorderLayout.CENTER); // 刷新组件 EventQueue.invokeLater(TranslatorFrame.this::repaint); String theme = UIUtil.isUnderDarcula() ? “dark” : “light”; jbCefBrowser.loadURL(“http://localhost:7654/index.html?theme=” + theme); } else { jbCefClient.dispose(); jbCefBrowser.dispose(); } super.setVisible(b); } public static TranslatorFrame getInstance() { return TranslatorFrame.SingletonRegistryHolder.INSTANCE; } private static class SingletonRegistryHolder { private static final TranslatorFrame INSTANCE = new TranslatorFrame(); }}

新建com.github.clyoudu.sqltrans.jecefhandler包,创建 LocalRequestHandler.java

package com.github.clyoudu.sqltrans.jcefhandler;import org.cef.browser.CefBrowser;import org.cef.browser.CefFrame;import org.cef.handler.CefRequestHandlerAdapter;import org.cef.handler.CefResourceRequestHandler;import org.cef.misc.BoolRef;import org.cef.network.CefRequest;/** * LocalRequestHandler. * * @author leichen * @since 1.0, 2022/12/28 5:10 PM */public class LocalRequestHandler extends CefRequestHandlerAdapter { @Override public CefResourceRequestHandler getResourceRequestHandler(CefBrowser browser, CefFrame frame, CefRequest request, boolean isNavigation, boolean isDownload, String requestInitiator, BoolRef disableDefaultHandling) { return new LocalResourceRequestHandler(); }}

LocalResourceRequestHandler.java

package com.github.clyoudu.sqltrans.jcefhandler;import org.cef.browser.CefBrowser;import org.cef.browser.CefFrame;import org.cef.handler.CefResourceHandler;import org.cef.handler.CefResourceRequestHandlerAdapter;import org.cef.network.CefRequest;/** * LocalResourceRequestHandler. * * @author leichen * @since 1.0, 2022/12/28 5:11 PM */public class LocalResourceRequestHandler extends CefResourceRequestHandlerAdapter { private static final String TRANSLATE = “translate”; private static final String LOCALHOST = “http://localhost:7654/”; @Override public CefResourceHandler getResourceHandler(CefBrowser browser, CefFrame frame, CefRequest request) { String url = request.getURL(); String file = url.replace(LOCALHOST, “”).replaceAll(“\\?.*”, “”); if (file.equals(TRANSLATE)) { return new HttpClientResourceHandler(request.getPostData()); } return new LocalStaticResourceHandler(file); }}

StCefResourceHandlerAdapter.java

package com.github.clyoudu.sqltrans.jcefhandler;import java.nio.ByteBuffer;import java.nio.charset.StandardCharsets;import java.util.Arrays;import org.cef.callback.CefCallback;import org.cef.handler.CefResourceHandlerAdapter;import org.cef.misc.IntRef;import org.cef.network.CefRequest;/** * StCefResourceHandlerAdapter. * * @author leichen * @since 1.0, 2022/12/28 5:16 PM */public class StCefResourceHandlerAdapter extends CefResourceHandlerAdapter { String html; int startPos = 0; @Override public boolean processRequest(CefRequest request, CefCallback callback) { startPos = 0; callback.Continue(); return true; } @Override public boolean readResponse(byte[] dataOut, int bytesToRead, IntRef intRef, CefCallback callback) { byte[] bytes = html.getBytes(StandardCharsets.UTF_8); int length = bytes.length; if (startPos >= length) { return false; } int endPos = startPos + bytesToRead; byte[] dataToSend = (endPos > length) ? Arrays.copyOfRange(bytes, startPos, length) : Arrays.copyOfRange(bytes, startPos, endPos); ByteBuffer result = ByteBuffer.wrap(dataOut); result.put(dataToSend); intRef.set(dataToSend.length); startPos = endPos; return true; }}

LocalStaticResourceHandler.java

package com.github.clyoudu.sqltrans.jcefhandler;import java.io.IOException;import java.io.InputStream;import org.apache.commons.compress.utils.IOUtils;import org.cef.misc.IntRef;import org.cef.misc.StringRef;import org.cef.network.CefResponse;/** * StaticResourceHandler. * * @author leichen * @since 1.0, 2022/12/28 5:13 PM */public class LocalStaticResourceHandler extends StCefResourceHandlerAdapter { private final String file; public LocalStaticResourceHandler(String file) { this.file = file; InputStream fileInputStream = LocalStaticResourceHandler.class.getClassLoader().getResourceAsStream(“html/” + file); try { html = new String(IOUtils.toByteArray(fileInputStream)); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void getResponseHeaders(CefResponse response, IntRef responseLength, StringRef redirectUrl) { responseLength.set(html.length()); String ext = file.substring(file.lastIndexOf(‘.’) + 1); switch (ext) { case “html”: response.setMimeType(“text/html”); break; case “js”: response.setMimeType(“text/javascript; charset=utf-8”); break; case “css”: response.setMimeType(“text/css; charset=utf-8”); break; default: break; } response.setStatus(200); }}

HttpClientResourceHandler.java

package com.github.clyoudu.sqltrans.jcefhandler;import java.io.IOException;import java.util.ArrayList;import java.util.Vector;import java.util.stream.Collectors;import okhttp3.MediaType;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.RequestBody;import okhttp3.Response;import org.cef.misc.IntRef;import org.cef.misc.StringRef;import org.cef.network.CefPostData;import org.cef.network.CefPostDataElement;import org.cef.network.CefResponse;/** * HttpClientResourceHandler. * * @author leichen * @since 1.0, 2022/12/28 5:18 PM */public class HttpClientResourceHandler extends StCefResourceHandlerAdapter { private static final OkHttpClient CLIENT = new OkHttpClient().newBuilder().build(); public HttpClientResourceHandler(CefPostData postData) { Vector elements = new Vector(); postData.getElements(elements); CefPostDataElement el = elements.get(0); int numBytes = el.getBytesCount(); byte[] readBytes = new byte[numBytes]; el.getBytes(numBytes, readBytes); String readString = new String(readBytes).trim(); String[] stringPairs = readString.split(“&”); java.util.List formData = new ArrayList(); for (String s : stringPairs) { String[] params = s.split(“=”); if (params.length formData.add(params[0] + “=” + params[1]); } } MediaType mediaType = MediaType.parse(“application/x-www-form-urlencoded; charset=UTF-8”); RequestBody body = RequestBody.create(mediaType, formData.stream().collect(Collectors.joining(“&”))); Request request = new Request.Builder().url(“https://www.jooq.org/translate/translate”).method(“POST”, body) .addHeader(“content-type”, “application/x-www-form-urlencoded; charset=UTF-8”).build(); try (Response response = CLIENT.newCall(request).execute();) { html = response.body().string(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void getResponseHeaders(CefResponse response, IntRef responseLength, StringRef redirectUrl) { responseLength.set(html.length()); response.setMimeType(“text/html; charset=UTF-8”); response.setHeaderByName(“content-encoding”, “gzip”, true); response.setHeaderByName(“cache-control”, “no-store, no-cache, must-revalidate”, true); response.setStatus(200); }}

新建com.github.clyoudu.sqltrans.actions包,新建PopupWindowAction.java

package com.github.clyoudu.sqltrans.actions;import com.intellij.openapi.actionSystem.AnAction;import com.intellij.openapi.actionSystem.AnActionEvent;import org.jetbrains.annotations.NotNull;import com.github.clyoudu.sqltrans.frame.TranslatorFrame;/** * PopupWindowAction. * * @author leichen * @since 1.0, 2022/12/27 5:33 PM */public class PopupWindowAction extends AnAction { @Override public void actionPerformed(@NotNull AnActionEvent event) { TranslatorFrame translatorFrame = TranslatorFrame.getInstance(); if (translatorFrame.isVisible()) { translatorFrame.requestFocus(); } else { translatorFrame.setVisible(true); } }}

修改resource/plugin.xml

com.clyoudu.sqltrans.sql-translator Sql-translator clyoudu com.intellij.modules.platform

jcefhandler主要逻辑为:

打开插件时,默认访问虚拟路径:http://localhost:7654/index.html拦截所有请求,静态资源一律返回/resource/html路径下相同URI的文件内容当请求路径为translate时,使用HttpClient发起代理请求,访问JOOQ SQL Translate接口

整个插件项目结构

.└── main ├── java │ └── com │ └── szkingdom │ └── sqltrans │ └── sqltranslator │ ├── actions │ │ └── PopupWindowAction.java │ ├── frame │ │ └── TranslatorFrame.java │ └── jcefhandler │ ├── HttpClientResourceHandler.java │ ├── LocalRequestHandler.java │ ├── LocalResourceRequestHandler.java │ ├── LocalStaticResourceHandler.java │ └── StCefResourceHandlerAdapter.java └── resources ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg └── html ├── asset-manifest.json ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static ├── css │ ├── main.3f1d99eb.css │ └── main.3f1d99eb.css.map ├── js │ ├── 5651.a6ce13cf.chunk.js │ ├── 5651.a6ce13cf.chunk.js.map │ ├── … │ └── main.ae1dd244.js.map └── media └── logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg

双击右侧的Gradle->Run Plugin 插件效果如下: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述


比丘资源网 » RingUI + JCEF开发IDEA插件

发表回复

提供最优质的资源集合

立即查看 了解详情