做一个简单的java编辑器

您所在的位置:网站首页 Java编辑器有哪些 做一个简单的java编辑器

做一个简单的java编辑器

2023-12-24 06:29| 来源: 网络整理| 查看: 265

最近闲来无事,对于之前放置不理的language server又起了兴趣,研究了一下,搞了一个简单的java编辑器,因为心血来潮,功能比较简单,只支持打开单个java文件,编辑(错误提示,自动补全等都有)和保存。主要使用了monaco-editor,monaco-languageclient,electron,vue和eclipse.jdt.ls。在网上没找到多少中文的相关内容,在这里简单记录一些自己的心得。

什么是language server protocol

Adding features like auto complete, go to definition, or documentation on hover for a programming language takes significant effort. Traditionally this work had to be repeated for each development tool, as each tool provides different APIs for implementing the same feature. A Language Server is meant to provide the language-specific smarts and communicate with development tools over a protocol that enables inter-process communication. The idea behind the Language Server Protocol (LSP) is to standardize the protocol for how such servers and development tools communicate. This way, a single Language Server can be re-used in multiple development tools, which in turn can support multiple languages with minimal effort. LSP is a win for both language providers and tooling vendors!

这里引用一些微软的官方解释,简单总结一下,语言服务器协议 (LSP) 的想法是标准化此类服务器和开发工具如何通信的协议。 这样,单个语言服务器可以在多个开发工具中重复使用,从而可以轻松地支持多种语言。 我从微软的语言服务器实现文档中找到了java的服务器实现,从中选择了eclipse.jdt.ls作为我们app选用的java语言服务器。

启动java language server 下载eclipse.jdt.ls

进入eclipse.jdt.ls的git仓库,参考readme即可。功能很强大,可以看到支持单独文件,也支持maven项目,我们这里只使用了单独文件的功能。 在这里插入图片描述

我选择了最新的snapshot版本,进入下载页面下载,然后将压缩包解压到/opt/jdt-language-server文件夹下面,文件夹里面的内容如下。 在这里插入图片描述

命令行启动

然后按照文档的指引启动即可,这里面./plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar要替换成你自己的jar文件,我下载的版本是这个,-configuration ./config_mac \这个因为我是mac系统,所以配置成这样,除此之外还有config_win和config_linux。

java \ -Declipse.application=org.eclipse.jdt.ls.core.id1 \ -Dosgi.bundles.defaultStartLevel=4 \ -Declipse.product=org.eclipse.jdt.ls.core.product \ -Dlog.level=ALL \ -Xmx1G \ --add-modules=ALL-SYSTEM \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.lang=ALL-UNNAMED \ -jar ./plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar \ -configuration ./config_mac \

但是,这样启动的language server只支持标准输入和标准输出,我们在命令行启动的这个server并没有办法应用于网络环境。

搭建一个node服务器

官方文档说可以配置环境变量CLIENT_PORT启用socket,我失败了,没有找到解决方案。最后反复查找,受到Aaaaash的启发,最后决定使用node搭建一个服务器。大概思路是使用node的子进程启动这个java进程,然后监听socket,写到java子进程,并将子进程的输出写到socket。我本来打算直接抄他的服务器代码的,emmm,不太好用,自己改了改,我nodejs不太擅长,勉强看看吧,具体代码如下。

const cp = require("child_process") const express = require("express") const glob = require("glob") const WebSocket = require("ws").WebSocket const url = require("url") const CONFIG_DIR = process.platform === 'darwin' ? 'config_mac' : process.platform === 'linux' ? 'config_linux' : 'config_win' const BASE_URI = '/opt/jdt-language-server' const PORT = 5036 const launchersFound = glob.sync('**/plugins/org.eclipse.equinox.launcher_*.jar', {cwd: `${BASE_URI}`}) if (launchersFound.length === 0 || !launchersFound) { throw new Error('**/plugins/org.eclipse.equinox.launcher_*.jar Not Found!') } const params = [ '-Xmx1G', '-Xms1G', '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Dlog.level=ALL', '-Declipse.product=org.eclipse.jdt.ls.core.product', '-jar', `${BASE_URI}/${launchersFound[0]}`, '-configuration', `${BASE_URI}/${CONFIG_DIR}` ] let app = express() let server = app.listen(PORT) let ws = new WebSocket.Server({ noServer: true, perMessageDeflate: false }) server.on('upgrade', function (request, socket, head) { let pathname = request.url ? url.parse(request.url).pathname : undefined console.log(pathname) if (pathname === '/java-lsp') { ws.handleUpgrade(request, socket, head, function (webSocket) { let lspSocket = { send: function (content) { return webSocket.send(content, function (error) { if (error) { throw error } }) }, onMessage: function (cb) { return webSocket.on('message', cb) }, onError: function (cb) { return webSocket.on('error', cb) }, onClose: function (cb) { return webSocket.on('close', cb) }, dispose: function () { return webSocket.close() } } if (webSocket.readyState === webSocket.OPEN) { launch(lspSocket) } else { webSocket.on('open', function () { return launch(lspSocket) }) } }) } }) function launch(socket) { let process = cp.spawn('java', params) let data = '' let left = 0, start = 0, last = 0 process.stdin.setEncoding('utf-8') socket.onMessage(function (data) { console.log(`Receive:${data.toString()}`) process.stdin.write('Content-Length: ' + data.length + '\n\n') process.stdin.write(data.toString()) }) socket.onClose(function () { console.log('Socket Closed') process.kill() }) process.stdout.on('data', function (respose) { data += respose.toString() let end = 0 for(let i = last; i if(left == 0) { start = i } left++ } else if(data.charAt(i) == '}') { left-- if(left == 0) { let json = data.substring(start, i + 1) end = i + 1 console.log(`Send: ${json}`) socket.send(json) } } } data = data.substring(end) last = data.length - end start -= end }) process.stderr.on('data', function (respose) { console.error(`Error: ${respose.toString()}`) }) }

要注意的是:

monaco-editor发送过来的信息和子进程需要的信息之间不太匹配需要处理,monaco-editor发送过来的是Buffer对象,没有content-length的信息,子进程输出的信息是Content-length和json数据,因此把信息写到子进程的输入时需要加上Conten-length信息,从子进程的输出读数据写到socket的时候需要过滤掉Conten-length信息。另外数据很长的时候子进程的输出是一段一段的,需要拼接。

我们使用node index.js启动这个node进程,就得到了一个可以处理socket链接的java language server。

创建一个java编辑器 创建一个vue项目 vue create java-editor

添加monaco编辑器相关依赖

npm install [email protected] --save npm install [email protected] --save-dev npm install monaco-languageclient --save npm install @codingame/monaco-jsonrpc --save 添加electron-builder vue add electron-builder electron-builder install-app-deps

然后在vue.config.js文件里面添加plugin:

configureWebpack: { plugins: [ new MonacoWebpackPlugin({ languages: ['javascript', 'css', 'html', 'typescript', 'json', 'java'] }) ] } 创建Editor

参考monaco-languageclient的使用样例我们在components里面添加一个Editor.vue文件。

Please Open A Java File const {ipcRenderer} = window.require('electron') import { listen } from "@codingame/monaco-jsonrpc" import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js' import 'monaco-editor/esm/vs/basic-languages/java/java.contribution' const { MonacoLanguageClient, CloseAction, ErrorAction, MonacoServices, createConnection } = require('monaco-languageclient') export default { name: 'JavaEditor', data() { return { editor: null, websocket: null, model: null } }, methods: { createLanguageClient(connection) { return new MonacoLanguageClient({ name: "Java LSP client", clientOptions: { documentSelector: ['java'], errorHandler: { error: () => ErrorAction.Continue, closed: () => CloseAction.DoNotRestart } }, connectionProvider: { get: (errorHandler, closeHandler) => { return Promise.resolve(createConnection(connection, errorHandler, closeHandler)) } } }) }, createModel (filePath) { let fileContent = window.require('fs').readFileSync(filePath, 'utf-8').toString() return monaco.editor.createModel(fileContent, 'java', monaco.Uri.file(filePath)) } }, mounted() { let self = this //注册 Monaco language client 的服务 MonacoServices.install(monaco) //监听打开文件的event,创建model ipcRenderer.on('open', (event, filePath) => { let first = !this.model let model = monaco.editor.getModel(monaco.Uri.file(filePath)) if (!model) { model = this.createModel(filePath) } this.model = model //第一次打开的话,要创建编辑器,链接到language server if(first) { this.$nextTick(() => { this.editor = monaco.editor.create(this.$refs.main, { model: model }) //这里这个url是之前启动的java language server的地址 const url = 'ws://127.0.0.1:5036/java-lsp' this.websocket = new WebSocket(url) listen({ webSocket: self.websocket, onConnection: connection => { console.log("connect") const client = self.createLanguageClient(connection); const disposable = client.start() connection.onClose(() => disposable.dispose()); console.log(`Connected to "${url}" and started the language client.`); } }) }) } else { this.editor.setModel(model) } }) //监听save事件,保存文件 ipcRenderer.on('save', () => { if(this.model) { window.require('fs').writeFileSync(this.model.uri.fsPath, this.model.getValue()) } }) } } h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; }

修改App.vue文件,把Editor加入App.vue文件。

A Simple Jave Editor import Editor from './components/Editor.vue' export default { name: 'App', components: { Editor } } #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } body { margin: 0; } 配置electron菜单

修改background.js文件,这是之前electron-builder添加的electron的主进程,加入menu配置,主要是添加打开文件,保存文件的菜单。

'use strict' import { app, protocol, BrowserWindow, Menu, dialog } from 'electron' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' const isDevelopment = process.env.NODE_ENV !== 'production' // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } } ]) async function createWindow() { // Create the browser window. const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false, enableRemoteModule: true } }) if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) if (!process.env.IS_TEST) win.webContents.openDevTools() } else { createProtocol('app') // Load the index.html when not in development win.loadURL('app://./index.html') } const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] }] : []), { label: 'File', //打开文件和保存文件的menu定义 submenu: [ { label: 'Open File', accelerator: "CmdOrCtrl+O", click: () => { if (win && !win.isDestroyed()) { dialog.showOpenDialog(win, { properties: ['openFile'], filters: [{name: 'Java', extensions: ['java']},] }).then(result => { if (!result.canceled) { win.webContents.send('open', result.filePaths[0]) } }).catch(err => { console.log(err) }) } } }, {label: 'Save File', accelerator: "CmdOrCtrl+S", click: () => { if(win && !win.isDestroyed()) { win.webContents.send('save') } }}, isMac ? { role: 'close' } : { role: 'quit' } ] }, { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ { role: 'pasteAndMatchStyle' }, { role: 'delete' }, { role: 'selectAll' }, { type: 'separator' }, { label: 'Speech', submenu: [ { role: 'startSpeaking' }, { role: 'stopSpeaking' } ] } ] : [ { role: 'delete' }, { type: 'separator' }, { role: 'selectAll' } ]) ] }, { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ { type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' } ] : [ { role: 'close' } ]) ] }, { role: 'help', submenu: [ { label: 'Learn More', click: async () => { const { shell } = require('electron') await shell.openExternal('https://electronjs.org') } } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } // Quit when all windows are closed. app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { if (isDevelopment && !process.env.IS_TEST) { // Install Vue Devtools try { await installExtension(VUEJS_DEVTOOLS) } catch (e) { console.error('Vue Devtools failed to install:', e.toString()) } } createWindow() }) // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { app.quit() } }) } else { process.on('SIGTERM', () => { app.quit() }) } } 启动运行

我们的editor就搭建好了,然后启动构建运行即可。

#启动 npm run electron:serve #构建 npm run electron:build

启动之后界面如下: 在这里插入图片描述 打开一个本地java文件之后: 在这里插入图片描述

总结

最后,总结一下过程中遇到的问题

1.版本问题

monaco-editor和monaco-editor-webpack-plugin的版本是有对应关系的,刚开始由于默认使用最新版本0.33.0和7.0.1导致出现了很多错误,各种改版本,遇到了大概如下问题:

Error: Cannot find module 'vs/editor/contrib/gotoSymbol/goToCommands' Error: Cannot find module 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching' Error: Cannot find module 'vs/editor/contrib/anchorSelect/anchorSelect' ERROR in ./node_modules/monaco-editor/esm/vs/language/css/monaco.contribution.js 29:15 Module parse failed: Unexpected token (29:15) You may need an appropriate loader to handle this file type.

这是monaco-editor-webpack-plugin主页表注的对应关系表: 在这里插入图片描述 按照这个表来说,最新版本应该是可以的,我也没太搞明白,经过反复实验,最后选定了[email protected][email protected],解决了上述的问题。

另外,反复使用npm install更新版本遇到了下面的问题

Error: Cyclic dependency toposort/index.js:53:9) Uncaught TypeError: Converting circular structure to JSON

删除node_modules文件夹,重新install就好了。

2.monaco-languageclient使用问题

按照官网的指示使用monaco-languageclient时,遇到了如下问题:

Uncaught Error: Cannot find module 'vscode' __dirname is not defined

参考官网的changelog,要在vue.config.js里面添加alias:

configureWebpack: { resolve: { alias : { 'vscode': require.resolve('monaco-languageclient/lib/vscode-compatibility') } } }

另外,MonacoServices.install的使用根据版本不同改过很多次,要根据具体版本决定怎么用,我之前用错了,发生过以下问题:

TypeError: model.getLanguageId is not a function TypeError: Cannot read property 'getModels' of undefined

具体可以参考官网的changelog。 在这里插入图片描述

3.electron的问题

我之前使用electron-vue都是直接使用模板创建的,但是,vue更新了,模板已经很多年没有更新了,这回先创建vue然后添加的electron,就遇到了奇怪的问题:

Uncaught ReferenceError: __dirname is not defined

查找资料让我改创建window时候的webPreferences里面的参数,改成如下的样子。

const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false, enableRemoteModule: true } })

然后,出现了新的问题。

TypeError: fs.existsSync is not a function(anonymous function) node_modules/electron/index.js:6

细心的小伙伴可能发现了,我上面代码里面的引用很多使用的window.require而不是require,使用window.require可以解决node的模块找不到的问题,我对前端不是太懂,反正好用了,就直接这么用了,有了解详情的欢迎大家分享,一起学习,共同进步。

源代码 java language sever的源代码,参考 这儿.java editor的源代码,参考 这儿.


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3