From 7add109ad5e3c879778ba4c05f955ff406a68dba Mon Sep 17 00:00:00 2001 From: QCQCQC <1220204124@zust.edu.cn> Date: Wed, 23 Oct 2024 15:58:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E5=90=AF=E7=94=A8lsp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esbuild.js | 146 ++++++++++++++++---------------- package.json | 5 +- pnpm-lock.yaml | 51 +++++++++++ src/client/xmlLanguageClient.ts | 77 +++++++++++++++++ src/extension.ts | 7 +- src/server/xmlLanguageServer.ts | 132 +++++++++++++++++++++++++++++ 6 files changed, 344 insertions(+), 74 deletions(-) create mode 100644 src/client/xmlLanguageClient.ts create mode 100644 src/server/xmlLanguageServer.ts diff --git a/esbuild.js b/esbuild.js index 162cd77..b193fec 100644 --- a/esbuild.js +++ b/esbuild.js @@ -1,9 +1,9 @@ -const esbuild = require("esbuild"); +const esbuild = require('esbuild'); -const production = process.argv.includes("--production"); -const watch = process.argv.includes("--watch"); -const path = require("path"); -const fs = require("fs").promises; +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); +const path = require('path'); +const fs = require('fs').promises; /** * 递归复制目录 @@ -11,92 +11,96 @@ const fs = require("fs").promises; * @param {string} dest 目标目录 */ async function copyDir(src, dest) { - await fs.mkdir(dest, { recursive: true }); - let entries = await fs.readdir(src, { withFileTypes: true }); + await fs.mkdir(dest, { recursive: true }); + let entries = await fs.readdir(src, { withFileTypes: true }); - for (let entry of entries) { - let srcPath = path.join(src, entry.name); - let destPath = path.join(dest, entry.name); + for (let entry of entries) { + let srcPath = path.join(src, entry.name); + let destPath = path.join(dest, entry.name); - if (entry.isDirectory()) { - await copyDir(srcPath, destPath); - } else { - await fs.copyFile(srcPath, destPath); - console.log(`Copied ${srcPath} to ${destPath}`); - } - } + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + console.log(`Copied ${srcPath} to ${destPath}`); + } + } } /** * @type {import('esbuild').Plugin} */ const copyTemplatesPlugin = { - name: "copy-templates", - setup(build) { - build.onEnd(async () => { - const srcDir = path.join(__dirname, "src", "templates"); - const destDir = path.join(__dirname, "dist", "templates"); + name: 'copy-templates', + setup(build) { + build.onEnd(async () => { + const srcDir = path.join(__dirname, 'src', 'templates'); + const destDir = path.join(__dirname, 'dist', 'templates'); - try { - await copyDir(srcDir, destDir); - console.log("Templates copied successfully"); - } catch (err) { - console.error("Error copying templates:", err); - } - }); - }, + try { + await copyDir(srcDir, destDir); + console.log('Templates copied successfully'); + } catch (err) { + console.error('Error copying templates:', err); + } + }); + }, }; /** * @type {import('esbuild').Plugin} */ const esbuildProblemMatcherPlugin = { - name: "esbuild-problem-matcher", + name: 'esbuild-problem-matcher', - setup(build) { - build.onStart(() => { - console.log("[watch] build started"); - }); - build.onEnd((result) => { - result.errors.forEach(({ text, location }) => { - console.error(`✘ [ERROR] ${text}`); - console.error( - ` ${location.file}:${location.line}:${location.column}:` - ); - }); - console.log("[watch] build finished"); - }); - }, + setup(build) { + build.onStart(() => { + console.log('[watch] build started'); + }); + build.onEnd((result) => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + console.error( + ` ${location.file}:${location.line}:${location.column}:` + ); + }); + console.log('[watch] build finished'); + }); + }, }; async function main() { - const ctx = await esbuild.context({ - entryPoints: ["src/extension.ts", "src/utils/analyzeWorker.ts"], - bundle: true, - format: "cjs", - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: "node", - outdir: "dist", - entryNames: "[dir]/[name]", - external: ["vscode"], - logLevel: "silent", - plugins: [ - /* add to the end of plugins array */ - esbuildProblemMatcherPlugin, - copyTemplatesPlugin, - ], - }); - if (watch) { - await ctx.watch(); - } else { - await ctx.rebuild(); - await ctx.dispose(); - } + const ctx = await esbuild.context({ + entryPoints: [ + 'src/extension.ts', + 'src/utils/analyzeWorker.ts', + // 'src/server/xmlLanguageServer.ts', + ], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outdir: 'dist', + entryNames: '[dir]/[name]', + external: ['vscode'], + logLevel: 'silent', + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin, + copyTemplatesPlugin, + ], + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } } main().catch((e) => { - console.error(e); - process.exit(1); + console.error(e); + process.exit(1); }); diff --git a/package.json b/package.json index 1245d86..06cb4d3 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,10 @@ "dependencies": { "glob": "^11.0.0", "handlebars": "^4.7.8", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12" }, "description": "OAK框架辅助开发插件", "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8b01a5..20d7caa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,15 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver-textdocument: + specifier: ^1.0.12 + version: 1.0.12 devDependencies: '@types/lodash': specifier: ^4.17.12 @@ -1999,6 +2008,27 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + webidl-conversions@5.0.0: resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} engines: {node: '>=8'} @@ -4267,6 +4297,27 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vscode-jsonrpc@8.2.0: {} + + vscode-languageclient@9.0.1: + dependencies: + minimatch: 5.1.6 + semver: 7.6.3 + vscode-languageserver-protocol: 3.17.5 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + webidl-conversions@5.0.0: {} whatwg-encoding@3.1.1: diff --git a/src/client/xmlLanguageClient.ts b/src/client/xmlLanguageClient.ts new file mode 100644 index 0000000..b9e5153 --- /dev/null +++ b/src/client/xmlLanguageClient.ts @@ -0,0 +1,77 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { workspace, ExtensionContext } from 'vscode'; + +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from 'vscode-languageclient/node'; + +let client: LanguageClient; + +function createLanguageServer() { + // 服务器是在独立的进程中启动的 + let serverModule = path.join(__dirname, 'server', 'xmlLanguageServer.js'); + + if (!fs.existsSync(serverModule)) { + console.error('Could not find server module'); + return; + } + + // 服务器的调试选项 + // --inspect=6009: 在Node的调试器中运行服务器 + // --nolazy: 不要延迟加载 + let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; + + // 如果扩展在调试模式下运行,那么调试服务器选项 + // 否则运行正常的服务器 + let serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions, + }, + }; + + // 控制语言客户端的选项 + let clientOptions: LanguageClientOptions = { + // 为语言服务器注册xml文件 + documentSelector: [{ scheme: 'file', language: 'xml' }], + synchronize: { + // 通知服务器关于文件更改的事件 + fileEvents: workspace.createFileSystemWatcher('**/.clientrc'), + }, + }; + + // 创建语言客户端并启动 + client = new LanguageClient( + 'xmlLanguageServer', + 'XML Language Server', + serverOptions, + clientOptions + ); +} + +export async function startAndWaitForReacy(): Promise { + createLanguageServer(); + return new Promise((resolve) => { + if (client) { + client.onNotification('xmlLanguageServer/ready', () => { + console.log('xmlLanguageServer is ready'); + resolve(); + }); + } + // 启动客户端。这也会启动服务器 + client.start(); + }); +} + +export function deactivateClient(): Thenable | undefined { + if (!client) { + return undefined; + } + return client.stop(); +} diff --git a/src/extension.ts b/src/extension.ts index bcefaf7..ef28637 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,10 @@ import entityProviders from './plugins/entityJump'; import { activateOakLocale, deactivateOakLocale } from './plugins/oakLocale'; import { startWorker, stopWorker, waitWorkerReady } from './utils/workers'; import { loadComponents } from './utils/components'; -import { activateOakComponentPropsLinkProvider, deactivateOakComponentPropsLinkProvider } from './plugins/oakComponent'; +import { + activateOakComponentPropsLinkProvider, + deactivateOakComponentPropsLinkProvider, +} from './plugins/oakComponent'; // 初始化配置 // 查找工作区的根目录中的oak.config.json文件,排除src和node_modules目录 @@ -123,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) { oakPathCompletion.oakPathCompletion, oakPathCompletion.oakPathDocumentLinkProvider, ...oakPathHighlighter, - entityProviders.documentLinkProvider, + entityProviders.documentLinkProvider ); createFileWatcher(context); } catch (error) { diff --git a/src/server/xmlLanguageServer.ts b/src/server/xmlLanguageServer.ts new file mode 100644 index 0000000..9da4795 --- /dev/null +++ b/src/server/xmlLanguageServer.ts @@ -0,0 +1,132 @@ +import { + createConnection, + TextDocuments, + ProposedFeatures, + InitializeParams, + TextDocumentPositionParams, + CompletionItem, + CompletionItemKind, + TextDocumentSyncKind, + DidChangeConfigurationNotification, + Hover, +} from 'vscode-languageserver/node'; + +import { TextDocument } from 'vscode-languageserver-textdocument'; + +// 创建一个连接来使用Node的IPC作为传输 +let connection = createConnection(ProposedFeatures.all); + +// 创建一个简单的文本文档管理器 +let documents: TextDocuments = new TextDocuments(TextDocument); + +let hasConfigurationCapability: boolean = false; +let hasWorkspaceFolderCapability: boolean = false; + +connection.onInitialize((params: InitializeParams) => { + let capabilities = params.capabilities; + + hasConfigurationCapability = !!( + capabilities.workspace && !!capabilities.workspace.configuration + ); + hasWorkspaceFolderCapability = !!( + capabilities.workspace && !!capabilities.workspace.workspaceFolders + ); + + return { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: { + resolveProvider: true, + }, + hoverProvider: true, + }, + }; +}); + +connection.onInitialized(() => { + if (hasConfigurationCapability) { + connection.client.register( + DidChangeConfigurationNotification.type, + undefined + ); + } + if (hasWorkspaceFolderCapability) { + connection.workspace.onDidChangeWorkspaceFolders((_event) => { + connection.console.log('Workspace folder change event received.'); + }); + } + + // 发送服务器就绪通知 + connection.sendNotification("xmlLanguageServer/ready"); +}); + +// 这个处理程序提供初始补全项。 +connection.onCompletion( + (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { + return [ + { + label: 'TypeScript', + kind: CompletionItemKind.Text, + data: 1, + }, + { + label: 'JavaScript', + kind: CompletionItemKind.Text, + data: 2, + }, + ]; + } +); + +// 这个处理程序解析补全项的附加信息。 +connection.onCompletionResolve((item: CompletionItem): CompletionItem => { + if (item.data === 1) { + item.detail = 'TypeScript details'; + item.documentation = 'TypeScript documentation'; + } else if (item.data === 2) { + item.detail = 'JavaScript details'; + item.documentation = 'JavaScript documentation'; + } + return item; +}); + +connection.onHover((params: TextDocumentPositionParams): Hover | null => { + const document = documents.get(params.textDocument.uri); + if (!document) { + return null; + } + + const text = document.getText(); + const position = params.position; + const offset = document.offsetAt(position); + + // 查找 {{attr}} 模式 + const regex = /{{(\w+)}}/g; + let match; + while ((match = regex.exec(text)) !== null) { + if (match.index <= offset && offset <= match.index + match[0].length) { + const attr = match[1]; + const type = getTypeForAttr(attr); + return { + contents: { + kind: 'markdown', + value: `Type of \`${attr}\`: \`${type}\``, + }, + }; + } + } + + return null; +}); + +function getTypeForAttr(attr: string): string { + // 这里应该实现实际的类型推断逻辑 + // 为了示例,我们返回一个模拟的类型 + return `MockType<${attr}>`; +} + +// 使文档管理器监听连接的所有相关事件 +documents.listen(connection); + +// 监听连接 +connection.listen();