websocket-text-relay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +29 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/docs/code-structure.md +77 -0
- package/docs/creating-text-editor-plugin.md +10 -0
- package/docs/dev-getting-started.md +8 -0
- package/index.js +87 -0
- package/package.json +45 -0
- package/src/language-server/JsonRpcInterface.js +126 -0
- package/src/language-server/JsonRpcInterface.test.js +496 -0
- package/src/language-server/LspReader.js +119 -0
- package/src/language-server/LspReader.test.js +508 -0
- package/src/language-server/LspWriter.js +44 -0
- package/src/language-server/LspWriter.test.js +179 -0
- package/src/language-server/constants.js +23 -0
- package/src/ui/css/fonts/Montserrat-Black.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Bold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraLight.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Light.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Medium.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Regular.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-SemiBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Thin.ttf +0 -0
- package/src/ui/css/fonts.css +54 -0
- package/src/ui/css/main.css +198 -0
- package/src/ui/index.html +36 -0
- package/src/ui/js/components/ActivityTimeseriesGraph.js +149 -0
- package/src/ui/js/components/HeaderSummary.js +23 -0
- package/src/ui/js/components/ServerStatus.js +31 -0
- package/src/ui/js/components/SessionLabels.js +171 -0
- package/src/ui/js/components/SessionWedges.js +107 -0
- package/src/ui/js/components/StatusRing.js +42 -0
- package/src/ui/js/components/grids.js +29 -0
- package/src/ui/js/index.js +117 -0
- package/src/ui/js/main.js +97 -0
- package/src/ui/js/util/DependencyManager.js +31 -0
- package/src/ui/js/util/EventEmitter.js +40 -0
- package/src/ui/js/util/WebsocketClient.js +76 -0
- package/src/ui/js/util/constants.js +9 -0
- package/src/ui/js/util/drawing.js +128 -0
- package/src/websocket-interface/WebsocketClient.js +56 -0
- package/src/websocket-interface/WebsocketInterface.js +71 -0
- package/src/websocket-interface/WtrSession.js +122 -0
- package/src/websocket-interface/httpServer.js +58 -0
- package/src/websocket-interface/sessionManager.js +107 -0
- package/src/websocket-interface/util.js +12 -0
- package/src/websocket-interface/websocketApi.js +116 -0
- package/src/websocket-interface/websocketServer.js +18 -0
- package/start.js +4 -0
package/.eslintrc
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"extends": [
|
|
4
|
+
"eslint:recommended"
|
|
5
|
+
],
|
|
6
|
+
"rules": {
|
|
7
|
+
"semi": ["error", "never"],
|
|
8
|
+
"indent": ["error", 2],
|
|
9
|
+
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
|
10
|
+
"no-unused-vars": 1,
|
|
11
|
+
"no-return-assign": 0,
|
|
12
|
+
"multiline-ternary": 0,
|
|
13
|
+
"object-curly-spacing": 0,
|
|
14
|
+
"object-property-newline": 0,
|
|
15
|
+
"object-curly-newline": 0,
|
|
16
|
+
"quotes": 0,
|
|
17
|
+
"quote-props": 0,
|
|
18
|
+
"import/no-absolute-path": 0
|
|
19
|
+
},
|
|
20
|
+
"env": {
|
|
21
|
+
"node": true,
|
|
22
|
+
"browser": true,
|
|
23
|
+
"es2024": true
|
|
24
|
+
},
|
|
25
|
+
"parserOptions": {
|
|
26
|
+
"sourceType": "module",
|
|
27
|
+
"ecmaVersion": "latest"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Niels Nielsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# websocket-text-relay (WTR)
|
|
2
|
+
|
|
3
|
+
## Alpha
|
|
4
|
+
|
|
5
|
+
Note: This tool is still in alpha state. Some features like the VsCode extension and vite plugin have not been published yet. Documentation is still a work in progress
|
|
6
|
+
|
|
7
|
+
This application connects to your text editor using the Language Server Protocol (LSP) and then starts a websocket
|
|
8
|
+
server that front end clients can connect to and subscribe to file change events. This allows users to see their
|
|
9
|
+
front end changes evaluated immediately as they type, without having to first save the file to disk or reload the browser.
|
|
10
|
+
It's a similar concept to sites like CodePen and JsFiddle, except you can develop locally using your own text editor with all
|
|
11
|
+
of your personalized plugins instead of having to use an in browser code editor.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### 1. Install the extension for your text editor
|
|
16
|
+
|
|
17
|
+
WTR currently has plugins for Neovim and VSCode.
|
|
18
|
+
* Installation instructions for Neovim
|
|
19
|
+
* Installation instructions for VSCode
|
|
20
|
+
|
|
21
|
+
If you wish to create a plugin for a new editor, the process is fairly straight forward if the editor has
|
|
22
|
+
LSP support. See the [developer guide to creating a WTR text editor plugin](./docs/creating-text-editor-plugin.md)
|
|
23
|
+
|
|
24
|
+
### 2. Connect to the websocket server from the front end application
|
|
25
|
+
|
|
26
|
+
To use this on a professional level project that gives you the option to use modules, typescript, and react, I recommend using vite along with
|
|
27
|
+
the plugin **vite-plugin-websocket-text-relay**. This plugin gives you all the power of vite when developing while also hooking
|
|
28
|
+
the live text updates into Vite's hot module reload system.
|
|
29
|
+
|
|
30
|
+
If you want to use this as a learning tool to play around with UI concepts using simple projects involving 1 html, css, and javascript file,
|
|
31
|
+
then check out this **Simple WTR reference project**. This is a great setup for following along with any short and focused web development tutorials.
|
|
32
|
+
|
|
33
|
+
And finally, the [status UI for this project](http://localhost:38378) was also created using live updates from websocket-text-relay.
|
|
34
|
+
In addition to giving you live feedback on the status and activity of the application, it is also meant to serve as a
|
|
35
|
+
reference UI project that is more complicated than a single javascript file, but still doesn't use any external dependencies.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Developing
|
|
39
|
+
|
|
40
|
+
If you are a developer looking to run websocket-text-relay from source and make modifications, follow the [Developer Getting Started Guide](./docs/dev-getting-started.md)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Developer Guide: Code Structure
|
|
2
|
+
|
|
3
|
+
## Core Logic: the start function
|
|
4
|
+
_websocket-text-relay_ merges two asynchronous interfaces. The the LSP server <-> text editor interface and
|
|
5
|
+
the websocket server <-> front end client interface.
|
|
6
|
+
|
|
7
|
+
Everything starts with the `startLanguageServer` function exported by the index.js file in the root of the project. The start function
|
|
8
|
+
is exported this way so it can be used as either a standalone command line application (the way the Neovim plugin interacts with the LSP server)
|
|
9
|
+
or as an imported library (the way the VSCode extension uses websocket-text-relay).
|
|
10
|
+
|
|
11
|
+
The function takes as parameters the input and output streams for the LSP server (they are optional and default to stdin and stdout if not included), and the port
|
|
12
|
+
for the http/websocket server (optional and defaults to 38378).
|
|
13
|
+
|
|
14
|
+
This function will connect the LSP server to the IO streams and spin up the websocket interface on the resolved port. Then it will wire up all
|
|
15
|
+
of the LSP lifecycle events and websocket API events to their handlers.
|
|
16
|
+
|
|
17
|
+
Every text editor instance will start up an instance of the websocket-text-relay app. Because it needs to host
|
|
18
|
+
an http server on a specific port for front end clients to connect to, only one instance of the app may act as the
|
|
19
|
+
websocket server. Every subsuquent instance must connect to that websocket server as a client to relay its text changes.
|
|
20
|
+
This is all abstracted away behind the websocket-interface, which is explained in greater detail below.
|
|
21
|
+
|
|
22
|
+
## Core Logic: The lsp and websocket events
|
|
23
|
+
|
|
24
|
+
LSP: `initailze` / WS: `init`
|
|
25
|
+
|
|
26
|
+
Whenever a new editor instance or front end client connects to the websocket, a websocket session is created for that client. These
|
|
27
|
+
can be seen as the labeled blue wedges around the ring in the status UI. When the lsp client from the editor sends an
|
|
28
|
+
initialize request to the language-server, it contains the editor name and editor processId. This data is then sent to the
|
|
29
|
+
main websocket server in an `init` message, where it can be viewed in the status UI (used to determine which sessions are editors and label each wedge with the editor name)
|
|
30
|
+
|
|
31
|
+
WS: `watch-file`
|
|
32
|
+
|
|
33
|
+
The front end client will send `watch-file` events to the websocket server to let it know what file changes it wants to subscribe to.
|
|
34
|
+
|
|
35
|
+
LSP: `wtr/update-open-files`
|
|
36
|
+
|
|
37
|
+
This event has to be manually sent by the editor plugin. Any time a file is opened or closed in the editor, the editor
|
|
38
|
+
will send a list of open files to the language-server.
|
|
39
|
+
|
|
40
|
+
LSP: `wtr/update-active-files`
|
|
41
|
+
|
|
42
|
+
The sessionManager will reconcile the files that the front end clients are watching with the files that the text
|
|
43
|
+
editor has open and determine which files the LSP client should attach to. Once attached the client will start sending `textDocument/didChange` events.
|
|
44
|
+
|
|
45
|
+
LSP: `textDocument/didChange`
|
|
46
|
+
|
|
47
|
+
Whenever an active text document is updated, send the latest changes to all watching clients via the weboscket-interface
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Language-Server
|
|
51
|
+
|
|
52
|
+
The language-server directory contains the code that handles communicating with the text editor over stdio. The main export for
|
|
53
|
+
interfacing with the LSP events is the JsonRpcInterface object in the JsonRpcInterface.js file. JsonRpc has 2
|
|
54
|
+
message types, requests and notifications. Therefore the JsonRpcInterface has 4 functions to send and receive these message types.
|
|
55
|
+
|
|
56
|
+
* onRequest - handle incoming requests from editor. Can be asynchronous, return a promise, or return a response synchronously.
|
|
57
|
+
* onNotification - handle incoming notifications from editor. No return value
|
|
58
|
+
* sendRequest - send outgoing request to editor. Returns a promise that resolves with the editor's response
|
|
59
|
+
* sendNotification - send outgoing notification. No response or return value
|
|
60
|
+
|
|
61
|
+
The code in LspReader and LspWriter handles all the lower level details of reading and writing JSON RPC messages. Test
|
|
62
|
+
driven development (TDD) was used to develop the data stream handlers and verify that the resulting interface behaves
|
|
63
|
+
as expected.
|
|
64
|
+
|
|
65
|
+
## Websocket-Interface
|
|
66
|
+
|
|
67
|
+
The websocket-interface directory contains the code for hosting and interacting with the websocket API. When the
|
|
68
|
+
application first starts up, the websocket-interface will attempt to spin up the http server on the specified port. If
|
|
69
|
+
successful, the application runs in server mode and all function calls made to the websocket-interface will be directed to itself.
|
|
70
|
+
If there is already a server running on the specified port, then the interface will connect to that server as a client. All
|
|
71
|
+
calls made through the websocket-interface will now be sent to the main websocket instance through the websocket client.
|
|
72
|
+
|
|
73
|
+
If the main server ever goes offline, each instance will attempt to start up an http server on the port. The first instance able
|
|
74
|
+
to start the http server will be the new main server, the rest of the instances will receive a port in use error from the OS and then
|
|
75
|
+
proceed to connect to the new server in client mode again.
|
|
76
|
+
|
|
77
|
+
All init messages are resent when a websocket client reconnects.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Developers Guide: Creating Text Editor Plugin
|
|
2
|
+
|
|
3
|
+
Placeholder guide for creating text editor plugin:
|
|
4
|
+
* Connecting the LSP client
|
|
5
|
+
* Seeing the session in the status UI
|
|
6
|
+
* Sending list of open files
|
|
7
|
+
* receiving list of active files (verify in status UI)
|
|
8
|
+
* Connecting the LSP client to the active text documents
|
|
9
|
+
* Subscribing to changes from a test UI
|
|
10
|
+
* Seeing updates in test UI and Status UI
|
package/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { JsonRpcInterface } from "./src/language-server/JsonRpcInterface.js"
|
|
2
|
+
import { WebsocketInterface } from "./src/websocket-interface/WebsocketInterface.js"
|
|
3
|
+
|
|
4
|
+
const DEFAULT_WS_PORT = 38378
|
|
5
|
+
|
|
6
|
+
const resolvePort = (portParam) => {
|
|
7
|
+
if (portParam != null) { return portParam }
|
|
8
|
+
if (process.env["websocket_text_relay_port"] != null) { return process.env["websocket_text_relay_port"] }
|
|
9
|
+
return DEFAULT_WS_PORT
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const resolveIoStreams = ({inputStreamParam, outputStreamParam}) => {
|
|
13
|
+
const inputStream = inputStreamParam || process.stdin
|
|
14
|
+
const outputStream = outputStreamParam || process.stdout
|
|
15
|
+
return {inputStream, outputStream}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fileProtocolPrefix = "file://"
|
|
19
|
+
|
|
20
|
+
const getNormalizedFileName = (uri) => {
|
|
21
|
+
if (uri.startsWith(fileProtocolPrefix)) { return uri.substring(fileProtocolPrefix.length) }
|
|
22
|
+
return uri
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lspInitializeResponse = {
|
|
26
|
+
serverInfo: {
|
|
27
|
+
name: "Websocket Text Relay LSP Server",
|
|
28
|
+
version: "1.0.0"
|
|
29
|
+
},
|
|
30
|
+
capabilities: {
|
|
31
|
+
textDocumentSync: 1 // full text sync
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const startLanguageServer = (options = {}) => {
|
|
36
|
+
const {inputStream: inputStreamParam, outputStream: outputStreamParam, port: portParam} = options
|
|
37
|
+
const jsonRpc = new JsonRpcInterface(resolveIoStreams({inputStreamParam, outputStreamParam}))
|
|
38
|
+
|
|
39
|
+
const wsInterface = new WebsocketInterface({port: resolvePort(portParam)})
|
|
40
|
+
|
|
41
|
+
wsInterface.emitter.on('message', (message) => {
|
|
42
|
+
if (message.method === "watch-editor-active-files") {
|
|
43
|
+
jsonRpc.sendNotification("wtr/update-active-files", {files: message.files})
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
jsonRpc.onRequest("initialize", (params) => {
|
|
48
|
+
const wsInitMessage = {
|
|
49
|
+
method: "init",
|
|
50
|
+
name: params.clientInfo?.name || "Unnamed Editor",
|
|
51
|
+
editorPid: params.processId,
|
|
52
|
+
lsPid: process.pid
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
wsInterface.sendInitMessage(wsInitMessage)
|
|
56
|
+
|
|
57
|
+
return lspInitializeResponse
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// use this function to hook into initalized event
|
|
61
|
+
// jsonRpc.onNotification("initialized", () => {
|
|
62
|
+
// })
|
|
63
|
+
|
|
64
|
+
jsonRpc.onRequest("shutdown", () => {
|
|
65
|
+
return null
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
jsonRpc.onNotification("exit", () => {
|
|
69
|
+
process.exit(0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
jsonRpc.onNotification('wtr/update-open-files', ({files}) => {
|
|
73
|
+
wsInterface.sendOpenFileList(files)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
jsonRpc.onNotification("textDocument/didOpen", (params) => {
|
|
77
|
+
const file = getNormalizedFileName(params.textDocument.uri)
|
|
78
|
+
const contents = params.textDocument.text
|
|
79
|
+
wsInterface.sendText({file, contents})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
jsonRpc.onNotification("textDocument/didChange", (params) => {
|
|
83
|
+
const file = getNormalizedFileName(params.textDocument.uri)
|
|
84
|
+
const contents = params.contentChanges[0]?.text
|
|
85
|
+
wsInterface.sendText({file, contents})
|
|
86
|
+
})
|
|
87
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "websocket-text-relay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An LSP server for sending live file updates from your text editor to the front end via websockets.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"websocket-text-relay": "./start.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"test": "vitest run --coverage",
|
|
11
|
+
"test-ui": "vitest --ui"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"directories": {
|
|
16
|
+
"doc": "docs"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+ssh://git@github.com/niels4/websocket-text-relay.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"websocket",
|
|
24
|
+
"text",
|
|
25
|
+
"relay",
|
|
26
|
+
"live",
|
|
27
|
+
"coding",
|
|
28
|
+
"LSP"
|
|
29
|
+
],
|
|
30
|
+
"author": "Niels Nielsen",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/niels4/websocket-text-relay/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/niels4/websocket-text-relay#readme",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@vitest/coverage-v8": "^1.4.0",
|
|
38
|
+
"@vitest/ui": "^1.4.0",
|
|
39
|
+
"eslint": "^8.57.0",
|
|
40
|
+
"vitest": "^1.4.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"ws": "^8.16.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { LspReader } from "./LspReader.js"
|
|
3
|
+
import { writeNotification, writeRequest, writeResponse } from './LspWriter.js'
|
|
4
|
+
import { invalidRequestErrorCode, invalidRequestErrorMessage, methodNotFoundErrorCode, methodNotFoundErrorMessage, unexpectedNotificationErrorCode,
|
|
5
|
+
unexpectedNotificationErrorMessage, unexpectedRequestErrorCode, unexpectedRequestErrorMessage } from './constants.js'
|
|
6
|
+
|
|
7
|
+
export class JsonRpcInterface {
|
|
8
|
+
constructor ({inputStream, outputStream}) {
|
|
9
|
+
this.inputStream = inputStream
|
|
10
|
+
this.outputStream = outputStream
|
|
11
|
+
|
|
12
|
+
this.outstandingRequests = new Map()
|
|
13
|
+
this.events = new EventEmitter()
|
|
14
|
+
this.notificationHandlers = new Map()
|
|
15
|
+
this.requestHandlers = new Map()
|
|
16
|
+
this.lspInputStream = new LspReader()
|
|
17
|
+
this.inputStream.pipe(this.lspInputStream)
|
|
18
|
+
|
|
19
|
+
this.lspInputStream.on('data', this._handleIncomingLspMessage.bind(this))
|
|
20
|
+
|
|
21
|
+
this.lspInputStream.on('parse-error', (error) => {
|
|
22
|
+
writeResponse(this.outputStream, null, error)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onNotification (method, handler) {
|
|
27
|
+
if (this.notificationHandlers.has(method)) {
|
|
28
|
+
throw new Error("Can only register one notification handler at a time. Duplicate method handlers for " + method)
|
|
29
|
+
}
|
|
30
|
+
this.notificationHandlers.set(method, handler)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeNotificationHandler (method) {
|
|
34
|
+
return this.notificationHandlers.delete(method)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onRequest (method, handler) {
|
|
38
|
+
if (this.requestHandlers.has(method)) {
|
|
39
|
+
throw new Error("Can only register one request handler at a time. Duplicate method handlers for " + method)
|
|
40
|
+
}
|
|
41
|
+
this.requestHandlers.set(method, handler)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removeRequestHandler (method) {
|
|
45
|
+
return this.requestHandlers.delete(method)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sendNotification (method, params) {
|
|
49
|
+
writeNotification(this.outputStream, method, params)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sendRequest (method, params) {
|
|
53
|
+
const responseHandler = {}
|
|
54
|
+
const promise = new Promise((resolve, reject) => {
|
|
55
|
+
responseHandler.resolve = resolve
|
|
56
|
+
responseHandler.reject = reject
|
|
57
|
+
})
|
|
58
|
+
const id = writeRequest(this.outputStream, method, params)
|
|
59
|
+
this.outstandingRequests.set(id, responseHandler)
|
|
60
|
+
return promise
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_handleIncomingLspMessage (message) {
|
|
64
|
+
const {method, params, id, error, result} = message
|
|
65
|
+
if (id != null) {
|
|
66
|
+
if (method != null) {
|
|
67
|
+
this._handleIncomingRequest(method, params, id)
|
|
68
|
+
} else {
|
|
69
|
+
this._handleRequestResponse(id, error, result)
|
|
70
|
+
}
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (method != null) {
|
|
75
|
+
this._handleIncomingNotification(method, params)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
const rpcError = {code: invalidRequestErrorCode, message: invalidRequestErrorMessage}
|
|
79
|
+
this.events.emit('rpc-error', rpcError)
|
|
80
|
+
writeResponse(this.outputStream, null, rpcError)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_handleIncomingNotification (method, params) {
|
|
84
|
+
const handler = this.notificationHandlers.get(method)
|
|
85
|
+
if (!handler) {
|
|
86
|
+
const errorObject = {code: methodNotFoundErrorCode, message: methodNotFoundErrorMessage, data: {method}}
|
|
87
|
+
this.events.emit("notification-error", errorObject)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
handler(params)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const errorObject = {code: unexpectedNotificationErrorCode, message: unexpectedNotificationErrorMessage, data: {method, params, error: error.message}}
|
|
94
|
+
this.events.emit("notification-error", errorObject)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _handleIncomingRequest (method, params, id) {
|
|
99
|
+
const handler = this.requestHandlers.get(method)
|
|
100
|
+
if (!handler) {
|
|
101
|
+
const errorObject = {code: methodNotFoundErrorCode, message: methodNotFoundErrorMessage, data: {method}}
|
|
102
|
+
writeResponse(this.outputStream, id, errorObject)
|
|
103
|
+
this.events.emit("request-error", {id, error: errorObject})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = await handler(params)
|
|
108
|
+
writeResponse(this.outputStream, id, null, result)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const errorObject = {code: unexpectedRequestErrorCode, message: unexpectedRequestErrorMessage, data: {method, params, error: error.message}}
|
|
111
|
+
this.events.emit("request-error", {id, error: errorObject})
|
|
112
|
+
writeResponse(this.outputStream, id, errorObject)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_handleRequestResponse (id, error, result) {
|
|
117
|
+
const responseHandler = this.outstandingRequests.get(id)
|
|
118
|
+
if (!responseHandler) { return }
|
|
119
|
+
this.outstandingRequests.delete(id)
|
|
120
|
+
if (error != null) {
|
|
121
|
+
responseHandler.reject(error)
|
|
122
|
+
} else {
|
|
123
|
+
responseHandler.resolve(result)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|