0


RingUI + JCEF开发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组件库有以下使用场景:

  1. 开发一个JetBrains风格的网页应用,比如开发了一个JetBrains插件,提供一个网站来介绍该插件;
  2. 开发一个基于Web的JetBrains插件,比如Markdown预览插件,这类插件如果使用Java Swing来实现,基本不可能,但是直接使用web组件却很方便;
  3. 开发一个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语法的转换,有如下限制和要求:

实现逻辑

  • 界面由单独的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.1
  • JDK: 11.0.11
  • React:18.2.0
  • node/npm: v16.18.1/8.19.2

开发流程

新建一个IDEA插件项目

假设项目名称为

sql-translator

在这里插入图片描述

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

新建一个前端项目

假设项目名称为

react-client
# 安装必要依赖npminstall--save react react-dom webpack webpack-dev-server html-webpack-plugin
npminstall--save--dev babel-loader babel-core babel-preset-react babel-preset-env@next

# 安装create-react-appnpminstall-g create-react-app

# 切换到插件项目根目录cd sql-translator

# 新建项目
create-react-app react-client

# 切换到前端项目根目录cd react-client

# 安装ring-uinpminstall @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';functionApp(){return(<Button onClick={()=> alertService.successMessage('Hello world')}>
      Click me
    </Button>);}exportdefault App;

在前端项目根目录下执行

npm start

,查看浏览器是否正确显示按钮,点击按钮后,右下角是否出现消息

丰富前端项目

在插件项目resource目录下新建一个html目录
项目中用到了

react-codemirror

axios

等,需要安装相关依赖:

npm install @uiw/react-codemirror axios @codemirror/lang-sql @jetbrains/icons

修改

package.json

中build脚本,方便每次构建完成后自动拷贝静态资源到目标位置

"scripts":{"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"exportdefaultclassAppextendsComponent{constructor(props){super(props);this.changeHandle =this.changeHandle.bind(this);this.selectHandle =this.selectHandle.bind(this);const params =newURLSearchParams(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 <column index>',key:'ALWAYS',type:'to-group-by-column-index'},{label:'Transform GROUP BY <column index> when not supported',key:'WHEN_NEEDED',type:'to-group-by-column-index'},{label:"Don't transform GROUP BY <column index>",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<ThemeProvider theme={jbTheme} passToPopups><div style={{padding:"10px"}} className="themed-wrapper"><Select className={"gap-right"} filter={true} label={"---"} selected={this.fromDialect.filter((i)=> i.key === snapshot['from-dialect'])} onChange={(e)=>this.setStateValue(e)} data={this.fromDialect}></Select><Icon glyph={compareIcon} className={"gap-large"} color={Icon.Color.BLUE}/><Select className={"gap-right"} filter={true} label={"---"} selected={this.toDialect.filter((i)=> i.key === snapshot['to-dialect'])} onChange={(e)=>this.setStateValue(e)} data={this.toDialect}></Select><Button primary onClick={this.translate} loader={this.state.loading}>
          Translate
        </Button><div className="ring-loader-inline" style={{marginLeft:"5px",display:this.state.loading ?"inline-block":"none"}}/><div style={{display:"flex"}}><div style={{flex:"1",marginRight:"5px",marginTop:"15px"}}><CodeMirror
              height="400px"
              extensions={[sql()]}
              theme={theme}
              width="calc(100vw/2 - 25px)"
              onChange={(value, view)=>{this.setState({sql: value})}}/><ThemeProvider theme={jbTheme} className="dark inputs"><Input size={Size.FULL} data-key="from-search-path" label="Search path, comma separated" onChange={this.changeHandle} value={snapshot["from-search-path"]}/><div className="themed-wrapper-with-top-gap"><Checkbox data-key="from-unknown-functions" label="Parse unknown functions" onChange={this.selectHandle} checked={snapshot["from-unknown-functions"]}/></div><Input size={Size.FULL} data-key="from-date-format" label="Date Format" onChange={this.changeHandle} value={snapshot["from-date-format"]}/><Input size={Size.FULL} data-key="from-timestamp-format" label="Timestamp Format" onChange={this.changeHandle} value={snapshot["from-timestamp-format"]}/><div className="themed-wrapper-with-top-gap"><Checkbox data-key="from-retain-comments-between-queries" label="Retain comments between queries" onChange={this.selectHandle} checked={snapshot["from-retain-comments-between-queries"]}/></div><div className="themed-wrapper-with-top-gap"><Checkbox data-key="from-ignore-comments" label="Enable ignore comment syntax" onChange={this.selectHandle} checked={snapshot["from-ignore-comments"]}/></div><Input size={Size.FULL} data-key="from-ignore-comment-start" label="Ignore comment start token" onChange={this.changeHandle} value={snapshot["from-ignore-comment-start"]}/><Input size={Size.FULL} data-key="from-ignore-comment-stop" label="Ignore comment stop token" onChange={this.changeHandle} value={snapshot["from-ignore-comment-stop"]}/></ThemeProvider></div><div style={{flex:"1",marginLeft:"5px",marginTop:"15px"}}><CodeMirror
              value={this.state.translateResult}
              height="400px"
              width="calc(100vw/2 - 25px)"
              extensions={[sql()]}
              theme={theme}/><ThemeProvider theme={jbTheme} className="dark inputs"><Select size={Size.FULL} selectedLabel="Keywords" selected={this.toKeywords.filter((i)=> i.key ===this.state['to-keywords'])} onChange={(e)=>this.setStateValue(e)} data={this.toKeywords}></Select><Select size={Size.FULL} selectedLabel="Identifier Case" selected={this.identifierCase.filter((i)=> i.key ===this.state['to-name-case'])} onChange={(e)=>this.setStateValue(e)} data={this.identifierCase}></Select><Select size={Size.FULL} selectedLabel="Identifier Quoting" selected={this.identifierQuoting.filter((i)=> i.key ===this.state['to-name-quoted'])} onChange={(e)=>this.setStateValue(e)} data={this.identifierQuoting}></Select><Select size={Size.FULL} selectedLabel="Bind variables" selected={this.bindVariables.filter((i)=> i.key ===this.state['to-param-type'])} onChange={(e)=>this.setStateValue(e)} data={this.bindVariables}></Select><Select size={Size.FULL} selectedLabel="Patterns (experimental)" selected={this.toPatterns.filter((i)=> i.key ===this.state['to-patterns'])} onChange={(e)=>this.setStateValue(e)} data={this.toPatterns}></Select><Select size={Size.FULL} selectedLabel="Join style" selected={this.joinStyle.filter((i)=> i.key ===this.state['to-join-style'])} onChange={(e)=>this.setStateValue(e)} data={this.joinStyle}></Select><Select size={Size.FULL} selectedLabel="QUALIFY" selected={this.qualify.filter((i)=> i.key ===this.state['to-qualify'])} onChange={(e)=>this.setStateValue(e)} data={this.qualify}></Select><Select size={Size.FULL} selectedLabel="ROWNUM" selected={this.rownum.filter((i)=> i.key ===this.state['to-rownum'])} onChange={(e)=>this.setStateValue(e)} data={this.rownum}></Select><Select size={Size.FULL} selectedLabel="inline CTE" selected={this.inlineCte.filter((i)=> i.key ===this.state['to-inline-cte'])} onChange={(e)=>this.setStateValue(e)} data={this.inlineCte}></Select><Select size={Size.FULL} selectedLabel="GROUP BY <column index>" selected={this.groupByColumnIndex.filter((i)=> i.key ===this.state['to-group-by-column-index'])} onChange={(e)=>this.setStateValue(e)} data={this.groupByColumnIndex}></Select><Select size={Size.FULL} selectedLabel="Remove unnecessary arithmetic" selected={this.unnecessaryArithmetic.filter((i)=> i.key ===this.state['to-unnecessary-arithmetic'])} onChange={(e)=>this.setStateValue(e)} data={this.unnecessaryArithmetic}></Select><Select size={Size.FULL} selectedLabel="AS keyword in SELECT" selected={this.toFieldAs.filter((i)=> i.key ===this.state['to-field-as'])} onChange={(e)=>this.setStateValue(e)} data={this.toFieldAs}></Select><Select size={Size.FULL} selectedLabel="AS keyword in FROM" selected={this.toTableAs.filter((i)=> i.key ===this.state['to-table-as'])} onChange={(e)=>this.setStateValue(e)} data={this.toTableAs}></Select><Select size={Size.FULL} selectedLabel="INNER keyword in JOIN" selected={this.toInnerKeyword.filter((i)=> i.key ===this.state['to-inner-keyword'])} onChange={(e)=>this.setStateValue(e)} data={this.toInnerKeyword}></Select><Select size={Size.FULL} selectedLabel="OUTER keyword in JOIN" selected={this.toOuterKeyword.filter((i)=> i.key ===this.state['to-outer-keyword'])} onChange={(e)=>this.setStateValue(e)} data={this.toOuterKeyword}></Select></ThemeProvider></div></div></div></ThemeProvider>};}
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);}

前端主要逻辑为:

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

:

packagecom.github.clyoudu.sqltrans.frame;importjava.awt.*;importjavax.swing.*;importcom.intellij.ui.jcef.JBCefBrowser;importcom.intellij.ui.jcef.JBCefClient;importcom.intellij.util.ui.UIUtil;importcom.github.clyoudu.sqltrans.jcefhandler.LocalRequestHandler;/**
 * TranslatorFrame.
 *
 * @author leichen
 * @since 1.0, 2022/12/27 5:28 PM
 */publicclassTranslatorFrameextendsJFrame{privateJBCefBrowser jbCefBrowser;privateJBCefClient jbCefClient;privatefinalJPanel dialogPanel;privateTranslatorFrame(){super("SQLTranslator");
        dialogPanel =newJPanel(newBorderLayout());add(dialogPanel);}@OverridepublicvoidsetVisible(boolean b){if(b){setSize(1024,750);setLocationRelativeTo(null);
            jbCefBrowser =newJBCefBrowser();
            jbCefClient = jbCefBrowser.getJBCefClient();
            jbCefClient.addRequestHandler(newLocalRequestHandler(), 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);}publicstaticTranslatorFramegetInstance(){returnTranslatorFrame.SingletonRegistryHolder.INSTANCE;}privatestaticclassSingletonRegistryHolder{privatestaticfinalTranslatorFrameINSTANCE=newTranslatorFrame();}}

新建

com.github.clyoudu.sqltrans.jecefhandler

包,创建

LocalRequestHandler.java
packagecom.github.clyoudu.sqltrans.jcefhandler;importorg.cef.browser.CefBrowser;importorg.cef.browser.CefFrame;importorg.cef.handler.CefRequestHandlerAdapter;importorg.cef.handler.CefResourceRequestHandler;importorg.cef.misc.BoolRef;importorg.cef.network.CefRequest;/**
 * LocalRequestHandler.
 *
 * @author leichen
 * @since 1.0, 2022/12/28 5:10 PM
 */publicclassLocalRequestHandlerextendsCefRequestHandlerAdapter{@OverridepublicCefResourceRequestHandlergetResourceRequestHandler(CefBrowser browser,CefFrame frame,CefRequest request,boolean isNavigation,boolean isDownload,String requestInitiator,BoolRef disableDefaultHandling){returnnewLocalResourceRequestHandler();}}
LocalResourceRequestHandler.java
packagecom.github.clyoudu.sqltrans.jcefhandler;importorg.cef.browser.CefBrowser;importorg.cef.browser.CefFrame;importorg.cef.handler.CefResourceHandler;importorg.cef.handler.CefResourceRequestHandlerAdapter;importorg.cef.network.CefRequest;/**
 * LocalResourceRequestHandler.
 *
 * @author leichen
 * @since 1.0, 2022/12/28 5:11 PM
 */publicclassLocalResourceRequestHandlerextendsCefResourceRequestHandlerAdapter{privatestaticfinalStringTRANSLATE="translate";privatestaticfinalStringLOCALHOST="http://localhost:7654/";@OverridepublicCefResourceHandlergetResourceHandler(CefBrowser browser,CefFrame frame,CefRequest request){String url = request.getURL();String file = url.replace(LOCALHOST,"").replaceAll("\\?.*","");if(file.equals(TRANSLATE)){returnnewHttpClientResourceHandler(request.getPostData());}returnnewLocalStaticResourceHandler(file);}}
StCefResourceHandlerAdapter.java
packagecom.github.clyoudu.sqltrans.jcefhandler;importjava.nio.ByteBuffer;importjava.nio.charset.StandardCharsets;importjava.util.Arrays;importorg.cef.callback.CefCallback;importorg.cef.handler.CefResourceHandlerAdapter;importorg.cef.misc.IntRef;importorg.cef.network.CefRequest;/**
 * StCefResourceHandlerAdapter.
 *
 * @author leichen
 * @since 1.0, 2022/12/28 5:16 PM
 */publicclassStCefResourceHandlerAdapterextendsCefResourceHandlerAdapter{String html;int startPos =0;@OverridepublicbooleanprocessRequest(CefRequest request,CefCallback callback){
        startPos =0;callback.Continue();returntrue;}@OverridepublicbooleanreadResponse(byte[] dataOut,int bytesToRead,IntRef intRef,CefCallback callback){byte[] bytes = html.getBytes(StandardCharsets.UTF_8);int length = bytes.length;if(startPos >= length){returnfalse;}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;returntrue;}}
LocalStaticResourceHandler.java
packagecom.github.clyoudu.sqltrans.jcefhandler;importjava.io.IOException;importjava.io.InputStream;importorg.apache.commons.compress.utils.IOUtils;importorg.cef.misc.IntRef;importorg.cef.misc.StringRef;importorg.cef.network.CefResponse;/**
 * StaticResourceHandler.
 *
 * @author leichen
 * @since 1.0, 2022/12/28 5:13 PM
 */publicclassLocalStaticResourceHandlerextendsStCefResourceHandlerAdapter{privatefinalString file;publicLocalStaticResourceHandler(String file){this.file = file;InputStream fileInputStream =LocalStaticResourceHandler.class.getClassLoader().getResourceAsStream("html/"+ file);try{
            html =newString(IOUtils.toByteArray(fileInputStream));}catch(IOException e){thrownewRuntimeException(e);}}@OverridepublicvoidgetResponseHeaders(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
packagecom.github.clyoudu.sqltrans.jcefhandler;importjava.io.IOException;importjava.util.ArrayList;importjava.util.Vector;importjava.util.stream.Collectors;importokhttp3.MediaType;importokhttp3.OkHttpClient;importokhttp3.Request;importokhttp3.RequestBody;importokhttp3.Response;importorg.cef.misc.IntRef;importorg.cef.misc.StringRef;importorg.cef.network.CefPostData;importorg.cef.network.CefPostDataElement;importorg.cef.network.CefResponse;/**
 * HttpClientResourceHandler.
 *
 * @author leichen
 * @since 1.0, 2022/12/28 5:18 PM
 */publicclassHttpClientResourceHandlerextendsStCefResourceHandlerAdapter{privatestaticfinalOkHttpClientCLIENT=newOkHttpClient().newBuilder().build();publicHttpClientResourceHandler(CefPostData postData){Vector<CefPostDataElement> elements =newVector<>();
        postData.getElements(elements);CefPostDataElement el = elements.get(0);int numBytes = el.getBytesCount();byte[] readBytes =newbyte[numBytes];
        el.getBytes(numBytes, readBytes);String readString =newString(readBytes).trim();String[] stringPairs = readString.split("&");java.util.List<String> formData =newArrayList<>();for(String s : stringPairs){String[] params = s.split("=");if(params.length <=1){
                formData.add(params[0]+"= ");}else{
                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 =newRequest.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){thrownewRuntimeException(e);}}@OverridepublicvoidgetResponseHeaders(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
packagecom.github.clyoudu.sqltrans.actions;importcom.intellij.openapi.actionSystem.AnAction;importcom.intellij.openapi.actionSystem.AnActionEvent;importorg.jetbrains.annotations.NotNull;importcom.github.clyoudu.sqltrans.frame.TranslatorFrame;/**
 * PopupWindowAction.
 *
 * @author leichen
 * @since 1.0, 2022/12/27 5:33 PM
 */publicclassPopupWindowActionextendsAnAction{@OverridepublicvoidactionPerformed(@NotNullAnActionEvent event){TranslatorFrame translatorFrame =TranslatorFrame.getInstance();if(translatorFrame.isVisible()){
            translatorFrame.requestFocus();}else{
            translatorFrame.setVisible(true);}}}

修改

resource/plugin.xml
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html --><idea-plugin><!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. --><id>com.clyoudu.sqltrans.sql-translator</id><!-- Public plugin name should be written in Title Case.
         Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name --><name>Sql-translator</name><!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. --><vendoremail="[email protected]"url="https://github.com/clyoudu">clyoudu</vendor><!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
         Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
         Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description --><description><![CDATA[
    Sql dialect translator with jooq online translator.
  ]]></description><!-- Product and plugin compatibility requirements.
         Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html --><depends>com.intellij.modules.platform</depends><!-- Extension points defined by the plugin.
         Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html --><extensionsdefaultExtensionNs="com.intellij"></extensions><actions><!-- Add your actions here --><actionid="com.github.clyoudu.sqltrans.actions.PopupWindowAction"class="com.github.clyoudu.sqltrans.actions.PopupWindowAction"text="SQL Translator"description="Open SQL translator window"><add-to-groupgroup-id="ToolsMenu"anchor="last"/><keyboard-shortcutkeymap="$default"first-keystroke="control shift alt T"/></action></actions></idea-plugin>

jcefhandler主要逻辑为:

  1. 打开插件时,默认访问虚拟路径:http://localhost:7654/index.html
  2. 拦截所有请求,静态资源一律返回/resource/html路径下相同URI的文件内容
  3. 当请求路径为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
在这里插入图片描述
插件效果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


本文转载自: https://blog.csdn.net/CL_YD/article/details/128477049
版权归原作者 CL有毒 所有, 如有侵权,请联系我们删除。

“RingUI + JCEF开发IDEA插件”的评论:

还没有评论