Skip to content

Commit

Permalink
#521 WIP pyright server and client
Browse files Browse the repository at this point in the history
  • Loading branch information
kaisalmen committed Aug 15, 2023
1 parent fe2cd88 commit 46b2f3e
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 8 deletions.
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ <h1>Examples</h1>
<br><br>
<a href="packages/examples/main/react.html">React Client Example</a>

<h2>Python</h2>
<a href="packages/examples/main/python.html">Python Pyright Client Example</a>

<h1>Verification</h1>
<h2>Webpack</h2>
Please start <b><code>npm run start:verify:webpack</code></b> beforehand:<br>
Expand Down
17 changes: 16 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/examples/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"monaco-editor-workers": "~0.41.0",
"monaco-languageclient": "~6.4.0",
"normalize-url": "~8.0.0",
"pyright": "~1.1.322",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"request-light": "~0.7.0",
Expand Down Expand Up @@ -57,6 +58,7 @@
"build:worker:langium": "esbuild ./src/langium/langiumServerWorker.js --bundle --tree-shaking=true --minify --format=iife --outfile=./dist/worker/langiumServerWorker.js",
"build": "npm run build:msg && npm run clean && npm run compile && npm run build:worker:statemachine && npm run build:worker:langium",
"start": "node --loader ts-node/esm src/server/main.ts",
"start:ext": "node --loader ts-node/esm src/server/main.ts --external"
"start:ext": "node --loader ts-node/esm src/server/main.ts --external",
"start:python": "node --loader ts-node/esm src/python/server.ts"
}
}
16 changes: 16 additions & 0 deletions packages/examples/main/python.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>

<head>
<title>Monaco Language Client Python Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
<h2>Monaco Language Client Python Example</h2>
<div id="container" style="width:800px;height:600px;border:1px solid grey"></div>
<script type="module" src="./src/python/client.ts"></script>
</body>

</html>
106 changes: 106 additions & 0 deletions packages/examples/main/src/python/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) 2018-2022 TypeFox GmbH (http://www.typefox.io). All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import 'monaco-editor/esm/vs/editor/editor.all.js';
import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
import * as vscode from 'vscode';
import 'vscode/default-extensions/theme-defaults';
import 'vscode/default-extensions/python';
import { updateUserConfiguration } from 'vscode/service-override/configuration';
import { createConfiguredEditor, createModelReference } from 'vscode/monaco';
import { initServices, MonacoLanguageClient } from 'monaco-languageclient';
import { CloseAction, ErrorAction, MessageTransports } from 'vscode-languageclient';
import { WebSocketMessageReader, WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc';
import { RegisteredFileSystemProvider, registerFileSystemOverlay, RegisteredMemoryFile } from 'vscode/service-override/files';

import { buildWorkerDefinition } from 'monaco-editor-workers';
buildWorkerDefinition('../../../node_modules/monaco-editor-workers/dist/workers/', new URL('', window.location.href).href, false);

const createWebSocket = (url: string): WebSocket => {
const webSocket = new WebSocket(url);
webSocket.onopen = () => {
const socket = toSocket(webSocket);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
const languageClient = createLanguageClient({
reader,
writer
});
languageClient.start();
reader.onClose(() => languageClient.stop());
};
return webSocket;
};

const createLanguageClient = (transports: MessageTransports): MonacoLanguageClient => {
return new MonacoLanguageClient({
name: 'Pyright Language Client',
clientOptions: {
// use a language id as a document selector
documentSelector: ['json'],
// disable the default error handler
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart })
},
synchronize: {
fileEvents: [vscode.workspace.createFileSystemWatcher('**')]
}
},
// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: () => {
return Promise.resolve(transports);
}
}
});
};

const run = async () => {
// init vscode-api
await initServices({
enableModelService: true,
enableThemeService: true,
enableTextmateService: true,
enableKeybindingsService: true,
configureConfigurationServiceConfig: {
defaultWorkspaceUri: '/tmp'
},
debugLogging: true
});

updateUserConfiguration(`{
"editor.fontSize": 14,
"workbench.colorTheme": "Default Dark Modern"
}`);

const fileSystemProvider = new RegisteredFileSystemProvider(false);
fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/tmp/hello.py'), 'print("Hello, World!")'));
registerFileSystemOverlay(1, fileSystemProvider);

// create the web socket and configure to start the language client on open
createWebSocket('ws://localhost:30000/pyright');

// register the JSON language with Monaco
const languageId = 'python';
monaco.languages.register({
id: languageId,
extensions: ['.py'],
aliases: ['PYTHON', 'python']
});

// create the model inside the workspace
const modelRef = await createModelReference(monaco.Uri.file('/tmp/hello.py'));
modelRef.object.setLanguageId(languageId);

// create monaco editor
createConfiguredEditor(document.getElementById('container')!, {
model: modelRef.object.textEditorModel,
automaticLayout: true
});
};

run();
14 changes: 14 additions & 0 deletions packages/examples/main/src/python/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pyright Python Language Server</title>
</head>

<body>
<h1>Hello Pyright</h1>
</body>

</html>
86 changes: 86 additions & 0 deletions packages/examples/main/src/python/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) 2018-2022 TypeFox GmbH (http://www.typefox.io). All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { WebSocketServer } from 'ws';
import { IncomingMessage } from 'http';
import { URL } from 'url';
import { Socket } from 'net';
import express from 'express';
import { IWebSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc';
import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrpc/server';
import { Message, InitializeRequest, InitializeParams } from 'vscode-languageserver';
import { getLocalDirectory } from '../server/fs-utils.js';

const launchLanguageServer = (socket: IWebSocket) => {
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);

// start the language server as an external process
const socketConnection = createConnection(reader, writer, () => socket.dispose());
const serverConnection = createServerProcess('PYRIGHT', 'npx', ['pyright', '--verbose', '--watch', '/tmp']);
if (serverConnection) {
forward(socketConnection, serverConnection, message => {
if (Message.isRequest(message)) {
console.log(message);
if (message.method === InitializeRequest.type.method) {
const initializeParams = message.params as InitializeParams;
initializeParams.processId = process.pid;
}
}
return message;
});
}
};

const run = () => {
process.on('uncaughtException', function(err: any) {
console.error('Uncaught Exception: ', err.toString());
if (err.stack) {
console.error(err.stack);
}
});

// create the express application
const app = express();
// server the static content, i.e. index.html
const dir = getLocalDirectory(import.meta.url);
app.use(express.static(dir));
// start the server
const server = app.listen(30000);
// create the web socket
const wss = new WebSocketServer({
noServer: true,
perMessageDeflate: false
});
server.on('upgrade', (request: IncomingMessage, socket: Socket, head: Buffer) => {
const baseURL = `http://${request.headers.host}/`;
const pathname = request.url ? new URL(request.url, baseURL).pathname : undefined;
if (pathname === '/pyright') {
wss.handleUpgrade(request, socket, head, webSocket => {
const socket: IWebSocket = {
send: content => webSocket.send(content, error => {
if (error) {
throw error;
}
}),
onMessage: cb => webSocket.on('message', (data) => {
console.log(data.toString());
cb(data);
}),
onError: cb => webSocket.on('error', cb),
onClose: cb => webSocket.on('close', cb),
dispose: () => webSocket.close()
};
// launch the server when the web socket is opened
if (webSocket.readyState === webSocket.OPEN) {
launchLanguageServer(socket);
} else {
webSocket.on('open', () => launchLanguageServer(socket));
}
});
}
});
};

run();
2 changes: 2 additions & 0 deletions packages/examples/main/src/python/workspace/src/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

print("Hello, World!")
6 changes: 3 additions & 3 deletions packages/examples/main/src/server/fs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { fileURLToPath } from 'url';
/**
* Solves: __dirname is not defined in ES module scope
*/
export function getLocalDirectory() {
const __filename = fileURLToPath(import.meta.url);
export const getLocalDirectory = (referenceUrl: string | URL) => {
const __filename = fileURLToPath(referenceUrl);
return dirname(__filename);
}
};
2 changes: 1 addition & 1 deletion packages/examples/main/src/server/json-server-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function launch(socket: IWebSocket) {
const asExternalProccess = process.argv.findIndex(value => value === '--external') !== -1;
if (asExternalProccess) {
// start the language server as an external process
const extJsonServerPath = resolve(getLocalDirectory(), '../../dist/server/ext-json-server.js');
const extJsonServerPath = resolve(getLocalDirectory(import.meta.url), '../../dist/server/ext-json-server.js');
const socketConnection = createConnection(reader, writer, () => socket.dispose());
const serverConnection = createServerProcess('JSON', 'node', [extJsonServerPath]);
if (serverConnection) {
Expand Down
3 changes: 2 additions & 1 deletion packages/examples/main/src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ process.on('uncaughtException', function(err: any) {
// create the express application
const app = express();
// server the static content, i.e. index.html
app.use(express.static(getLocalDirectory()));
const dir = getLocalDirectory(import.meta.url);
app.use(express.static(dir));
// start the server
const server = app.listen(3000);
// create the web socket
Expand Down
3 changes: 2 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default defineConfig(() => {
langiumClient: resolve(__dirname, 'packages/examples/main/langium_client.html'),
statemachineClient: resolve(__dirname, 'packages/examples/main/statemachine_client.html'),
browser: resolve(__dirname, 'packages/examples/main/browser.html'),
react: resolve(__dirname, 'packages/examples/main/react.html')
react: resolve(__dirname, 'packages/examples/main/react.html'),
python: resolve(__dirname, 'packages/examples/main/python.html')
}
}
},
Expand Down

0 comments on commit 46b2f3e

Please sign in to comment.