initial push
This commit is contained in:
parent
3078fbb9e6
commit
a03463b3f1
48
README.md
48
README.md
@ -1,3 +1,47 @@
|
||||
# Newtan-LSP
|
||||
# Newton LSP Server Bridge
|
||||
|
||||
Newtan LSP service
|
||||
## Overview
|
||||
This script provides a WebSocket bridge to interface with multiple Language Servers through clients via a single WebSocket connection.
|
||||
It supports various language servers concurrently and handles language-specific file extensions and initialization parameters.
|
||||
|
||||
## Features
|
||||
- Utilizes `vscode-jsonrpc` for LSP message handling.
|
||||
- Supports multiple language servers concurrently.
|
||||
- Support for IPC and stdio communication with language servers.
|
||||
- Easy language server configuration and initialization parameter modification.
|
||||
- Error handling and logging for language server processes.
|
||||
- Automatic file URI translation between server and client paths.
|
||||
- Temporary file creation for `textDocument/didOpen` events.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Install Node.js.
|
||||
2. Run 'npm install'.
|
||||
3. Configure the desired language servers in `defaultServers.js`.
|
||||
4. Run the script: `npm run start || start-verbose`.
|
||||
5. Connect to the WebSocket server on `ws://localhost:9999/<language_endpoint>`.
|
||||
6. Transmit LSP messages via the WebSocket connection.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add or modify language server configurations in `languageServers.js`:
|
||||
|
||||
```javascript
|
||||
exports.servers = [
|
||||
{
|
||||
endpointName: "language_endpoint", // WebSocket endpoint name
|
||||
args: ['executable_path', ['arg1', 'arg2', ...]],
|
||||
connectionType: "ipc" | "stdio", // Communication type
|
||||
relativePath: true | false, // Whether to use relative paths
|
||||
serverFileNameReplacePattern: { // Server file name regex replacements
|
||||
from: /pattern/,
|
||||
to: "replacement"
|
||||
},
|
||||
clientFileNameReplacePattern: { // Client file name regex replacements
|
||||
from: /pattern/,
|
||||
to: "replacement"
|
||||
}
|
||||
},
|
||||
...
|
||||
];
|
||||
```
|
179
index.js
Normal file
179
index.js
Normal file
@ -0,0 +1,179 @@
|
||||
const argv = require('yargs').argv;
|
||||
const fs = require('fs');
|
||||
const path = require("path");
|
||||
const {servers} = require("./languageServers");
|
||||
const {spawn} = require('child_process');
|
||||
const url = require('url');
|
||||
const WebSocket = require('ws');
|
||||
const verbose = argv.verbose || false;
|
||||
|
||||
const {
|
||||
IPCMessageReader,
|
||||
IPCMessageWriter,
|
||||
StreamMessageReader,
|
||||
StreamMessageWriter
|
||||
} = require('vscode-jsonrpc');
|
||||
const {
|
||||
formatPath,
|
||||
makeClientPath,
|
||||
makeServerPath
|
||||
} = require("./paths-utility");
|
||||
|
||||
|
||||
|
||||
const _port = 9999;
|
||||
const wss = new WebSocket.Server({port: _port});
|
||||
console.log("Started websocket server on port: ", _port);
|
||||
|
||||
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const pathname = url.parse(req.url).pathname;
|
||||
handleLanguageConnection(ws, pathname.substring(1));
|
||||
});
|
||||
|
||||
|
||||
|
||||
function handleLanguageConnection(ws, pathname) {
|
||||
const server = servers.find(server => server.endpointName === pathname);
|
||||
setupLanguageServer(ws, server);
|
||||
}
|
||||
|
||||
function setupLanguageServer(ws, server) {
|
||||
if (!server) return;
|
||||
|
||||
const {
|
||||
reader,
|
||||
writer
|
||||
} = startLanguageServer(server);
|
||||
|
||||
server.writer = writer;
|
||||
|
||||
reader.listen(message => {
|
||||
if (message.error) {
|
||||
console.error(server.nameEndsWith + ":");
|
||||
console.error(message.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log(`From server(${server.endpointName}): `);
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
processMessage(message, ws, server);
|
||||
});
|
||||
|
||||
ws.on('message', message => {
|
||||
let parsed = JSON.parse(message);
|
||||
|
||||
if (verbose) {
|
||||
console.log("From client: ");
|
||||
console.log(parsed);
|
||||
}
|
||||
|
||||
handleMessage(parsed, server);
|
||||
});
|
||||
}
|
||||
|
||||
function startLanguageServer(languageServer) {
|
||||
let env = process.env;
|
||||
const serverProcess = spawn(...languageServer.args, {env, shell: true});
|
||||
|
||||
serverProcess.stderr.on('data', data => {
|
||||
console.error(`${serverProcess.spawnfile} error: ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.on('exit', code => {
|
||||
fs.readdirSync("temp").forEach(file => {
|
||||
fs.unlinkSync("temp" + path.sep + file);
|
||||
});
|
||||
|
||||
console.log(`${serverProcess.spawnfile} exited with code ${code}`);
|
||||
});
|
||||
|
||||
serverProcess.on('error', err => {
|
||||
console.error(`Failed to start ${serverProcess.spawnfile}:`, err);
|
||||
});
|
||||
|
||||
let reader;
|
||||
let writer;
|
||||
|
||||
switch (languageServer.connectionType) {
|
||||
case "ipc":
|
||||
reader = new IPCMessageReader(serverProcess);
|
||||
writer = new IPCMessageWriter(serverProcess);
|
||||
|
||||
break;
|
||||
case "stdio":
|
||||
if (serverProcess.stdin !== null && serverProcess.stdout !== null) {
|
||||
reader = new StreamMessageReader(serverProcess.stdout);
|
||||
writer = new StreamMessageWriter(serverProcess.stdin);
|
||||
} else {
|
||||
throw 'The language server process does not have a valid stdin or stdout';
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw 'Unknown connection type...';
|
||||
}
|
||||
|
||||
return {
|
||||
reader,
|
||||
writer
|
||||
};
|
||||
}
|
||||
|
||||
function processMessage(message, ws, server) {
|
||||
if (message.params) {
|
||||
if (message.params.textDocument && message.params.textDocument.uri) {
|
||||
message.params.textDocument.uri = makeClientPath(
|
||||
message.params.textDocument.uri,
|
||||
server.clientFileNameReplacePattern
|
||||
);
|
||||
} else if (message.params.uri) {
|
||||
message.params.uri = makeClientPath(
|
||||
message.params.uri,
|
||||
server.clientFileNameReplacePattern
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
function handleMessage(parsed, server) {
|
||||
if (parsed.method) {
|
||||
switch (parsed.method) {
|
||||
case "initialize":
|
||||
let rootUri = formatPath(__dirname + path.sep + "temp");
|
||||
|
||||
if (!parsed.params || (!parsed.params.rootUri && !parsed.params.rootPath && !parsed.params.workspaceFolders)) {
|
||||
if (!fs.existsSync("temp")) {
|
||||
fs.mkdirSync("temp");
|
||||
}
|
||||
parsed.params.rootUri = rootUri;
|
||||
parsed.params.rootPath = __dirname + path.sep + "temp";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (parsed.params && parsed.params.textDocument && parsed.params.textDocument.uri) {
|
||||
parsed.params.textDocument.uri = makeServerPath(
|
||||
parsed.params.textDocument.uri,
|
||||
server.serverFileNameReplacePattern
|
||||
);
|
||||
|
||||
if (server && server.relativePath) {
|
||||
parsed.params.textDocument.uri = parsed.params.textDocument.uri.replace(__dirname + path.sep, "");
|
||||
}
|
||||
}
|
||||
|
||||
const writer = server?.writer;
|
||||
if (writer) {
|
||||
writer.write(parsed);
|
||||
}
|
||||
}
|
48
languageServers.js
Normal file
48
languageServers.js
Normal file
@ -0,0 +1,48 @@
|
||||
exports.servers = [
|
||||
{
|
||||
endpointName: "python",
|
||||
args: ["pylsp"],
|
||||
nameEndsWith: ".python",
|
||||
connectionType: "stdio",
|
||||
relativePath: false
|
||||
}, {
|
||||
endpointName: "go",
|
||||
args: [
|
||||
'gopls', ['-mode=stdio', '-remote=auto']
|
||||
],
|
||||
nameEndsWith: ".golang",
|
||||
connectionType: "stdio",
|
||||
relativePath: false,
|
||||
serverFileNameReplacePattern: {
|
||||
from: /.golang$/,
|
||||
to: ".go"
|
||||
},
|
||||
clientFileNameReplacePattern: {
|
||||
from: /.go$/,
|
||||
to: ".golang"
|
||||
},
|
||||
}, {
|
||||
endpointName: "c",
|
||||
args: [
|
||||
'clangd', ['--log=error']
|
||||
],
|
||||
nameEndsWith: ".c",
|
||||
connectionType: "stdio"
|
||||
}, {
|
||||
endpointName: "r",
|
||||
args: [
|
||||
'r', ['--slave', '-e', 'languageserver::run()']
|
||||
],
|
||||
nameEndsWith: ".r",
|
||||
connectionType: "stdio",
|
||||
relativePath: false
|
||||
}, {
|
||||
endpointName: "typescript",
|
||||
args: [
|
||||
'typescript-language-server', ['--stdio']
|
||||
],
|
||||
nameEndsWith: ".ts",
|
||||
connectionType: "stdio",
|
||||
relativePath: false
|
||||
},
|
||||
];
|
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Newton LSP Server Bridge",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"start-verbose": "node index.js --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vscode-jsonrpc": "^8.1.0",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
46
paths-utility.js
Normal file
46
paths-utility.js
Normal file
@ -0,0 +1,46 @@
|
||||
const path = require("path");
|
||||
const {URI} = require("vscode-uri");
|
||||
|
||||
|
||||
|
||||
function makeServerPath(fileName, replacement) {
|
||||
if (fileName.startsWith("file:")) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const serverPath = formatPath(__dirname + path.sep + "temp" + path.sep + fileName);
|
||||
if (replacement) {
|
||||
return serverPath.replace(
|
||||
replacement.from,
|
||||
replacement.to
|
||||
);
|
||||
}
|
||||
|
||||
return serverPath;
|
||||
}
|
||||
|
||||
function makeClientPath(filePath, replacement) {
|
||||
if (/^(file|https?):/.test(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
const clientPath = filePath.split(/[/\\]/).pop();
|
||||
if (replacement) {
|
||||
return clientPath.replace(
|
||||
replacement.from,
|
||||
replacement.to
|
||||
);
|
||||
}
|
||||
|
||||
return clientPath;
|
||||
}
|
||||
|
||||
function formatPath(filePath) {
|
||||
return URI.file(filePath).toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.formatPath = formatPath;
|
||||
exports.makeClientPath = makeClientPath;
|
||||
exports.makeServerPath = makeServerPath;
|
Loading…
Reference in New Issue
Block a user