mirror of
https://github.com/silvanshade/tower-lsp-web-demo.git
synced 2024-11-24 04:09:37 +00:00
Implement monaco editor and lsp server with features
This commit is contained in:
parent
d34b074c47
commit
24506849a5
52 changed files with 12355 additions and 1033 deletions
3
.cargo/config
Normal file
3
.cargo/config
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build]
|
||||
rustflags = "--cfg=web_sys_unstable_apis"
|
||||
target = "wasm32-unknown-unknown"
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
dist
|
23
.eslintrc.yaml
Normal file
23
.eslintrc.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
parser: "@typescript-eslint/parser"
|
||||
plugins:
|
||||
- "@typescript-eslint"
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:prettier/recommended
|
||||
- prettier
|
||||
overrides:
|
||||
- files: "webpack.config.js"
|
||||
env:
|
||||
"node": true
|
||||
- files: ["*.ts", "*.tsx"]
|
||||
extends:
|
||||
- plugin:@typescript-eslint/eslint-recommended
|
||||
- plugin:@typescript-eslint/recommended-requiring-type-checking
|
||||
- plugin:@typescript-eslint/recommended
|
||||
rules:
|
||||
"@typescript-eslint/no-inferrable-types": ["off"]
|
||||
parserOptions:
|
||||
project:
|
||||
- "./tsconfig.json"
|
||||
- "./packages/app/tsconfig.json"
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
packages/app/assets/wasm
|
||||
|
||||
### Generated by gibo (https://github.com/simonwhitaker/gibo)
|
||||
### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/Archives.gitignore
|
||||
|
||||
|
|
11
.prettierrc.yaml
Normal file
11
.prettierrc.yaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
bracketSpacing: true
|
||||
printWidth: 120
|
||||
semi: true
|
||||
singleQuote: false
|
||||
tabWidth: 2
|
||||
trailingComma: all
|
||||
useTabs: false
|
||||
overrides:
|
||||
- files: "*.ts"
|
||||
options:
|
||||
parser: typescript
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"livePreview.defaultPreviewPath": "/packages/app/assets/index.html"
|
||||
}
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/browser",
|
||||
"crates/language",
|
||||
"crates/server",
|
||||
]
|
||||
default-members = ["crates/browser"]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter-facade = { path = "../tree-sitter-facade" }
|
||||
web-tree-sitter-sys = { path = "../web-tree-sitter-sys" }
|
47
Makefile.toml
Normal file
47
Makefile.toml
Normal file
|
@ -0,0 +1,47 @@
|
|||
[config]
|
||||
default_to_workspace = false
|
||||
skip_core_tasks = true
|
||||
|
||||
[tasks.deps]
|
||||
script = '''
|
||||
cargo install wasm-bindgen-cli --version 0.2.80
|
||||
npm install
|
||||
'''
|
||||
|
||||
[tasks.build-server]
|
||||
script = '''
|
||||
cargo build
|
||||
wasm-bindgen --keep-debug --out-dir ./packages/app/assets/wasm --target web --typescript ./target/wasm32-unknown-unknown/debug/demo_lsp_browser.wasm
|
||||
'''
|
||||
|
||||
[tasks.build-app]
|
||||
script = '''
|
||||
npm run build --workspace=packages/app
|
||||
'''
|
||||
|
||||
[tasks.build]
|
||||
dependencies = ["build-server", "build-app"]
|
||||
|
||||
[tasks.clean-server]
|
||||
script = '''
|
||||
cargo clean
|
||||
'''
|
||||
|
||||
[tasks.clean-app]
|
||||
script = '''
|
||||
rm -rf packages/app/dist
|
||||
rm -rf packages/app/assets/wasm
|
||||
'''
|
||||
|
||||
[tasks.clean]
|
||||
dependencies = ["clean-server", "clean-app"]
|
||||
|
||||
[tasks.format]
|
||||
script = '''
|
||||
cargo +nightly fmt --all
|
||||
'''
|
||||
|
||||
[tasks.run]
|
||||
script = '''
|
||||
npm run app --workspace=packages/app
|
||||
'''
|
20
README.md
20
README.md
|
@ -1,28 +1,20 @@
|
|||
<div align="center">
|
||||
<h1><code>tower-lsp-wasm-example</code></h1>
|
||||
<h1><code>tower-lsp-web-demo</code></h1>
|
||||
<p>
|
||||
<strong>A minimal WASM target example for tower-lsp</strong>
|
||||
<strong>A minimal browser-hosted WASM demo for tower-lsp</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
cargo install wasm-bindgen-cli --version 0.2.80
|
||||
cd server
|
||||
RUSTFLAGS=--cfg=web_sys_unstable_apis cargo build --release --target wasm32-unknown-unknown
|
||||
wasm-bindgen --out-dir ../app/dist --target web --typescript ./target/wasm32-unknown-unknown/release/server.wasm
|
||||
cd ..
|
||||
cd app
|
||||
npm i
|
||||
npm run build
|
||||
cargo install cargo-make
|
||||
cargo make deps
|
||||
cargo make build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
cd app
|
||||
npm run app
|
||||
cargo make run
|
||||
```
|
||||
|
||||
After the browser window opens, you can try copying and pasting the listed messages into the `stdin` textarea and hitting the `send` button.
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-rows: 2fr 4fr 1fr 2fr 4fr;
|
||||
align-items: center;
|
||||
width: 50vh;
|
||||
height: 50vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25vh;
|
||||
}
|
||||
|
||||
.container textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
<title>tower-lsp-wasm-example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre>
|
||||
<!-- NOTE: included for easy copy and pasting into stdin textarea -->
|
||||
{"jsonrpc": "2.0", "method": "initialize", "params": { "capabilities": {}}, "id": 1}
|
||||
{"jsonrpc": "2.0", "method": "shutdown", "id": 2}
|
||||
{"jsonrpc": "2.0", "method": "exit"}
|
||||
</pre>
|
||||
|
||||
<div class="container">
|
||||
<h1>stdin</h1>
|
||||
<textarea id="stdin" autocomplete="off" spellcheck="off" wrap="off"></textarea>
|
||||
<button id="send-button">send</button>
|
||||
<h1>stdout</h1>
|
||||
<textarea id="stdout" autocomplete="off" spellcheck="off" wrap="off" readonly></textarea>
|
||||
</div>
|
||||
<script type="module">
|
||||
import init, { serve } from "./dist/server.js"
|
||||
import { LspStdin, LspStdout } from "./dist/index.js"
|
||||
|
||||
const stdin = LspStdin.create(document.getElementById("stdin"), document.getElementById("send-button"));
|
||||
const stdout = LspStdout.create(document.getElementById("stdout"));
|
||||
|
||||
await init();
|
||||
await serve(stdin, stdout);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
697
app/package-lock.json
generated
697
app/package-lock.json
generated
|
@ -1,697 +0,0 @@
|
|||
{
|
||||
"name": "tower-lsp-wasm-app",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tower-lsp-wasm-app",
|
||||
"version": "0.0.0",
|
||||
"license": "Apache-2.0 WITH LLVM-exception",
|
||||
"dependencies": {
|
||||
"http-server": "^14.1.0",
|
||||
"typescript": "^4.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-server": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.0.tgz",
|
||||
"integrity": "sha512-5lYsIcZtf6pdR8tCtzAHTWrAveo4liUlJdWc7YafwK/maPgYHs+VNP6KpCClmUnSorJrARVMXqtT055zBv11Yg==",
|
||||
"dependencies": {
|
||||
"basic-auth": "^2.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"corser": "^2.0.1",
|
||||
"he": "^1.2.0",
|
||||
"html-encoding-sniffer": "^3.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.2.5",
|
||||
"opener": "^1.5.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"secure-compare": "3.0.1",
|
||||
"union": "~0.5.0",
|
||||
"url-join": "^4.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"http-server": "bin/http-server"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
|
||||
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
|
||||
"dependencies": {
|
||||
"async": "^2.6.2",
|
||||
"debug": "^3.1.1",
|
||||
"mkdirp": "^0.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/secure-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||
"integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM="
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
|
||||
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/union": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||
"dependencies": {
|
||||
"qs": "^6.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"html-encoding-sniffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||
"requires": {
|
||||
"whatwg-encoding": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"http-server": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.0.tgz",
|
||||
"integrity": "sha512-5lYsIcZtf6pdR8tCtzAHTWrAveo4liUlJdWc7YafwK/maPgYHs+VNP6KpCClmUnSorJrARVMXqtT055zBv11Yg==",
|
||||
"requires": {
|
||||
"basic-auth": "^2.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"corser": "^2.0.1",
|
||||
"he": "^1.2.0",
|
||||
"html-encoding-sniffer": "^3.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.2.5",
|
||||
"opener": "^1.5.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"secure-compare": "3.0.1",
|
||||
"union": "~0.5.0",
|
||||
"url-join": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
|
||||
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
|
||||
"requires": {
|
||||
"async": "^2.6.2",
|
||||
"debug": "^3.1.1",
|
||||
"mkdirp": "^0.5.5"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"secure-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||
"integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
|
||||
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A=="
|
||||
},
|
||||
"union": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||
"requires": {
|
||||
"qs": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||
},
|
||||
"whatwg-encoding": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||
"requires": {
|
||||
"iconv-lite": "0.6.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"name": "tower-lsp-wasm-app",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"author": "silvanshade <silvanshade@users.noreply.github.com>",
|
||||
"license": "Apache-2.0 WITH LLVM-exception",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"app": "http-server -o"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-server": "^14.1.0",
|
||||
"typescript": "^4.7.2"
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
class LspStdin {
|
||||
static async *create(
|
||||
stdin: HTMLTextAreaElement,
|
||||
sendButton: HTMLButtonElement
|
||||
): AsyncGenerator<Uint8Array, never, void> {
|
||||
const encoder = new TextEncoder();
|
||||
while (true) {
|
||||
const bytes = await new Promise<Uint8Array>((resolve) => {
|
||||
sendButton.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
const payload = stdin.value;
|
||||
const message = `Content-Length: ${payload.length}\r\n\r\n${payload}`;
|
||||
stdin.value = "";
|
||||
resolve(encoder.encode(message));
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
yield bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: unused ReadableByteStream based implementation. See comments in server/src/lib.rs.
|
||||
//
|
||||
// class LspStdin {
|
||||
// static create(stdin: HTMLTextAreaElement, sendButton: HTMLButtonElement): ReadableStream {
|
||||
// const encoder = new TextEncoder();
|
||||
// return new ReadableStream({
|
||||
// type: "bytes" as any,
|
||||
// async start(controller) {
|
||||
// while (true) {
|
||||
// await new Promise<void>((resolve) => {
|
||||
// sendButton.addEventListener(
|
||||
// "click",
|
||||
// () => {
|
||||
// const payload = stdin.value;
|
||||
// const message = `Content-Length: ${payload.length}\r\n\r\n${payload}`;
|
||||
// const bytes = encoder.encode(message);
|
||||
// controller.enqueue(bytes);
|
||||
// stdin.value = "";
|
||||
// resolve();
|
||||
// },
|
||||
// { once: true }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
class LspStdout {
|
||||
static create(stdout: HTMLTextAreaElement): WritableStream {
|
||||
const decoder = new TextDecoder();
|
||||
return new WritableStream({
|
||||
async write(bytes) {
|
||||
const message = decoder.decode(bytes);
|
||||
const payload = message.replace(/^Content-Length:\s*\d+\s*/, "");
|
||||
stdout.value += payload;
|
||||
stdout.value += "\n";
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { LspStdin, LspStdout };
|
|
@ -1,105 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
"incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["DOM", "ES2022"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
"rootDir": "src", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
"newLine": "lf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"exclude": ["dist"],
|
||||
"include": ["src/**/*.ts"],
|
||||
}
|
33
crates/browser/Cargo.toml
Normal file
33
crates/browser/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
name = "demo-lsp-browser"
|
||||
version = "0.0.0"
|
||||
|
||||
[features]
|
||||
default = ["tower-lsp/runtime-agnostic"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
demo-lsp-language = { version = "0.0", path = "../language" }
|
||||
demo-lsp-server = { version = "0.0", path = "../server", default-features = false }
|
||||
futures = "0.3.21"
|
||||
js-sys = "0.3.57"
|
||||
tower-lsp = { version = "0.17.0", default-features = false }
|
||||
tree-sitter = { version = "*", package = "tree-sitter-facade" }
|
||||
wasm-bindgen = "0.2.80"
|
||||
wasm-bindgen-futures = { version = "0.4.30", features = ["futures-core-03-stream"] }
|
||||
wasm-streams = "0.2.3"
|
||||
web-tree-sitter-sys = "*"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.57"
|
||||
features = [
|
||||
"console",
|
||||
"HtmlTextAreaElement",
|
||||
"ReadableStream",
|
||||
"WritableStream",
|
||||
]
|
66
crates/browser/src/lib.rs
Normal file
66
crates/browser/src/lib.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
#![deny(clippy::all)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
use tower_lsp::{LspService, Server};
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use wasm_bindgen_futures::{stream::JsStream, JsFuture};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ServerConfig {
|
||||
into_server: js_sys::AsyncIterator,
|
||||
from_server: web_sys::WritableStream,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ServerConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream) -> Self {
|
||||
Self {
|
||||
into_server,
|
||||
from_server,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: we don't use web_sys::ReadableStream for input here because on the
|
||||
// browser side we need to use a ReadableByteStreamController to construct it
|
||||
// and so far only Chromium-based browsers support that functionality.
|
||||
|
||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
|
||||
#[wasm_bindgen]
|
||||
pub async fn serve(config: ServerConfig) -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
web_sys::console::log_1(&"server::serve".into());
|
||||
|
||||
let ServerConfig {
|
||||
into_server,
|
||||
from_server,
|
||||
} = config;
|
||||
|
||||
let language = demo_lsp_language::language::javascript().await.unwrap();
|
||||
JsFuture::from(web_tree_sitter_sys::Parser::init())
|
||||
.await
|
||||
.expect("failed to initialize tree-sitter");
|
||||
|
||||
let input = JsStream::from(into_server);
|
||||
let input = input
|
||||
.map_ok(|value| {
|
||||
value
|
||||
.dyn_into::<js_sys::Uint8Array>()
|
||||
.expect("could not cast stream item to Uint8Array")
|
||||
.to_vec()
|
||||
})
|
||||
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
|
||||
.into_async_read();
|
||||
|
||||
let output = JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
|
||||
let output = wasm_streams::WritableStream::from_raw(output);
|
||||
let output = output.try_into_async_write().map_err(|err| err.0)?;
|
||||
|
||||
let (service, messages) = LspService::new(|client| demo_lsp_server::Server::new(client, language));
|
||||
Server::new(input, output, messages).serve(service).await;
|
||||
|
||||
Ok(())
|
||||
}
|
16
crates/language/Cargo.toml
Normal file
16
crates/language/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
name = "demo-lsp-language"
|
||||
version = "0.0.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
thiserror = "1.0"
|
||||
js-sys = "0.3.57"
|
||||
tree-sitter = { version = "*", package = "tree-sitter-facade" }
|
||||
wasm-bindgen = { version = "=0.2.80", features = ["strict-macro"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3.57"
|
||||
web-tree-sitter-sys = "*"
|
16
crates/language/src/language.rs
Normal file
16
crates/language/src/language.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use anyhow::anyhow;
|
||||
pub async fn javascript() -> anyhow::Result<tree_sitter::Language> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
let bytes: &[u8] = include_bytes!("../../../node_modules/tree-sitter-javascript/tree-sitter-javascript.wasm");
|
||||
let promise = web_tree_sitter_sys::Language::load_bytes(&bytes.into());
|
||||
let future = JsFuture::from(promise);
|
||||
let value = future
|
||||
.await
|
||||
.map_err(|_| anyhow!("failed to load tree-sitter-javascript.wasm"))?;
|
||||
let inner = value.unchecked_into::<web_tree_sitter_sys::Language>();
|
||||
let result = inner.into();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub static ID: &'static str = "javascript";
|
5
crates/language/src/lib.rs
Normal file
5
crates/language/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#![deny(clippy::all)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
pub mod language;
|
||||
pub mod parser;
|
5
crates/language/src/parser.rs
Normal file
5
crates/language/src/parser.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub fn javascript(language: &tree_sitter::Language) -> anyhow::Result<tree_sitter::Parser> {
|
||||
let mut parser = tree_sitter::Parser::new()?;
|
||||
parser.set_language(language)?;
|
||||
Ok(parser)
|
||||
}
|
42
crates/server/Cargo.toml
Normal file
42
crates/server/Cargo.toml
Normal file
|
@ -0,0 +1,42 @@
|
|||
[package]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
name = "demo-lsp-server"
|
||||
version = "0.0.0"
|
||||
|
||||
[features]
|
||||
default = ["tower-lsp/runtime-agnostic"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
async-lock = "2.5.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
dashmap = "5.3.4"
|
||||
demo-lsp-language = { version = "0.0", path = "../language" }
|
||||
futures = "0.3.21"
|
||||
js-sys = "0.3.57"
|
||||
log = "0.4"
|
||||
lsp = { version = "0.93", package = "lsp-types" }
|
||||
lsp-text = { version = "0.5", features = ["tree-sitter"] }
|
||||
ropey = "1.5.0"
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tower-lsp = { version = "0.17.0", default-features = false }
|
||||
tree-sitter = { package = "tree-sitter-facade", version = "0.4" }
|
||||
wasm-bindgen = "0.2.80"
|
||||
wasm-bindgen-futures = { version = "0.4.30", features = ["futures-core-03-stream"] }
|
||||
wasm-streams = "0.2.3"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.57"
|
||||
features = [
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"Document",
|
||||
"ReadableStream",
|
||||
"Window",
|
||||
"WritableStream",
|
||||
]
|
11
crates/server/src/core.rs
Normal file
11
crates/server/src/core.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
pub mod document;
|
||||
pub mod error;
|
||||
pub mod session;
|
||||
pub mod syntax;
|
||||
pub mod text;
|
||||
|
||||
pub use demo_lsp_language::{language, parser};
|
||||
pub use document::*;
|
||||
pub use error::*;
|
||||
pub use session::*;
|
||||
pub use text::*;
|
64
crates/server/src/core/document.rs
Normal file
64
crates/server/src/core/document.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use async_lock::Mutex;
|
||||
use lsp_text::RopeExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Document {
|
||||
pub content: ropey::Rope,
|
||||
pub parser: tree_sitter::Parser,
|
||||
pub tree: tree_sitter::Tree,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub async fn open(
|
||||
session: Arc<crate::core::Session>,
|
||||
params: lsp::DidOpenTextDocumentParams,
|
||||
) -> anyhow::Result<Option<Self>> {
|
||||
let mut parser = crate::core::parser::javascript(&session.language)?;
|
||||
let content = ropey::Rope::from(params.text_document.text);
|
||||
let result = {
|
||||
let content = content.clone();
|
||||
let byte_idx = 0;
|
||||
let callback = content.chunk_walker(byte_idx).callback_adapter_for_tree_sitter();
|
||||
let old_tree = None;
|
||||
parser.parse_with(callback, old_tree)?
|
||||
};
|
||||
crate::core::syntax::update_channel(result.as_ref());
|
||||
Ok(result.map(|tree| crate::core::Document { content, parser, tree }))
|
||||
}
|
||||
|
||||
pub async fn change<'changes>(
|
||||
session: Arc<crate::core::Session>,
|
||||
uri: &lsp::Url,
|
||||
content: &ropey::Rope,
|
||||
) -> anyhow::Result<Option<tree_sitter::Tree>> {
|
||||
let result = {
|
||||
let parser = session.get_mut_parser(uri).await?;
|
||||
let mut parser = parser.lock().await;
|
||||
let text = content.chunks().collect::<String>();
|
||||
parser.parse(text, None)?
|
||||
};
|
||||
crate::core::syntax::update_channel(result.as_ref());
|
||||
if let Some(tree) = result {
|
||||
{
|
||||
let tree = tree.clone();
|
||||
*session.get_mut_tree(uri).await?.value_mut() = Mutex::new(tree);
|
||||
}
|
||||
Ok(Some(tree))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the language-id and textual content portion of the [`Document`].
|
||||
pub fn text(&self) -> crate::core::Text {
|
||||
crate::core::Text {
|
||||
content: self.content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum DocumentState {
|
||||
Closed,
|
||||
Opened,
|
||||
}
|
42
crates/server/src/core/error.rs
Normal file
42
crates/server/src/core/error.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::core;
|
||||
use thiserror::Error;
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum Error {
|
||||
#[error("ClientNotInitialzed")]
|
||||
ClientNotInitialized,
|
||||
#[error("core::SessionResourceNotFound: kind={kind:?}, uri={uri:?}")]
|
||||
SessionResourceNotFound {
|
||||
kind: core::session::SessionResourceKind,
|
||||
uri: lsp::Url,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct IntoJsonRpcError(pub anyhow::Error);
|
||||
|
||||
impl From<IntoJsonRpcError> for tower_lsp::jsonrpc::Error {
|
||||
fn from(error: IntoJsonRpcError) -> Self {
|
||||
let mut rpc_error = tower_lsp::jsonrpc::Error::internal_error();
|
||||
rpc_error.data = Some(serde_json::to_value(format!("{}", error.0)).unwrap());
|
||||
rpc_error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Error, IntoJsonRpcError};
|
||||
|
||||
#[test]
|
||||
fn from() {
|
||||
let error = Error::ClientNotInitialized;
|
||||
let error = error.into();
|
||||
|
||||
let mut expected = tower_lsp::jsonrpc::Error::internal_error();
|
||||
expected.data = Some(serde_json::to_value(format!("{}", error)).unwrap());
|
||||
|
||||
let actual: tower_lsp::jsonrpc::Error = IntoJsonRpcError(error).into();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
}
|
142
crates/server/src/core/session.rs
Normal file
142
crates/server/src/core/session.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use anyhow::anyhow;
|
||||
use async_lock::{Mutex, RwLock};
|
||||
use dashmap::{
|
||||
mapref::one::{Ref, RefMut},
|
||||
DashMap,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SessionResourceKind {
|
||||
Document,
|
||||
Parser,
|
||||
Tree,
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub server_capabilities: RwLock<lsp::ServerCapabilities>,
|
||||
pub client_capabilities: RwLock<Option<lsp::ClientCapabilities>>,
|
||||
client: Option<tower_lsp::Client>,
|
||||
pub language: tree_sitter::Language,
|
||||
pub document_states: DashMap<lsp::Url, crate::core::DocumentState>,
|
||||
document_texts: DashMap<lsp::Url, crate::core::Text>,
|
||||
document_parsers: DashMap<lsp::Url, Mutex<tree_sitter::Parser>>,
|
||||
document_trees: DashMap<lsp::Url, Mutex<tree_sitter::Tree>>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(client: Option<tower_lsp::Client>, language: tree_sitter::Language) -> Arc<Self> {
|
||||
let server_capabilities = RwLock::new(crate::server::capabilities());
|
||||
let client_capabilities = Default::default();
|
||||
let document_states = Default::default();
|
||||
let document_texts = Default::default();
|
||||
let document_parsers = Default::default();
|
||||
let document_trees = Default::default();
|
||||
Arc::new(Session {
|
||||
server_capabilities,
|
||||
client_capabilities,
|
||||
client,
|
||||
language,
|
||||
document_states,
|
||||
document_texts,
|
||||
document_parsers,
|
||||
document_trees,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn client(&self) -> anyhow::Result<&tower_lsp::Client> {
|
||||
self.client
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::core::Error::ClientNotInitialized.into())
|
||||
}
|
||||
|
||||
pub fn insert_document(&self, uri: lsp::Url, document: crate::core::Document) -> anyhow::Result<()> {
|
||||
let result = self.document_texts.insert(uri.clone(), document.text());
|
||||
debug_assert!(result.is_none());
|
||||
let result = self.document_parsers.insert(uri.clone(), Mutex::new(document.parser));
|
||||
debug_assert!(result.is_none());
|
||||
let result = self.document_trees.insert(uri, Mutex::new(document.tree));
|
||||
debug_assert!(result.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_document(&self, uri: &lsp::Url) -> anyhow::Result<()> {
|
||||
let result = self.document_texts.remove(uri);
|
||||
debug_assert!(result.is_some());
|
||||
let result = self.document_parsers.remove(uri);
|
||||
debug_assert!(result.is_some());
|
||||
let result = self.document_trees.remove(uri);
|
||||
debug_assert!(result.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn semantic_tokens_legend(&self) -> Option<lsp::SemanticTokensLegend> {
|
||||
let capabilities = self.server_capabilities.read().await;
|
||||
if let Some(capabilities) = &capabilities.semantic_tokens_provider {
|
||||
match capabilities {
|
||||
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options) => Some(options.legend.clone()),
|
||||
lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
|
||||
Some(options.semantic_tokens_options.legend.clone())
|
||||
},
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_text(&self, uri: &lsp::Url) -> anyhow::Result<Ref<'_, lsp::Url, crate::core::Text>> {
|
||||
self.document_texts.get(uri).ok_or_else(|| {
|
||||
let kind = SessionResourceKind::Document;
|
||||
let uri = uri.clone();
|
||||
crate::core::Error::SessionResourceNotFound { kind, uri }.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_mut_text(&self, uri: &lsp::Url) -> anyhow::Result<RefMut<'_, lsp::Url, crate::core::Text>> {
|
||||
self.document_texts.get_mut(uri).ok_or_else(|| {
|
||||
let kind = SessionResourceKind::Document;
|
||||
let uri = uri.clone();
|
||||
crate::core::Error::SessionResourceNotFound { kind, uri }.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_mut_parser(
|
||||
&self,
|
||||
uri: &lsp::Url,
|
||||
) -> anyhow::Result<RefMut<'_, lsp::Url, Mutex<tree_sitter::Parser>>> {
|
||||
self.document_parsers.get_mut(uri).ok_or_else(|| {
|
||||
let kind = SessionResourceKind::Parser;
|
||||
let uri = uri.clone();
|
||||
crate::core::Error::SessionResourceNotFound { kind, uri }.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_tree(&self, uri: &lsp::Url) -> anyhow::Result<Ref<'_, lsp::Url, Mutex<tree_sitter::Tree>>> {
|
||||
self.document_trees.get(uri).ok_or_else(|| {
|
||||
let kind = SessionResourceKind::Tree;
|
||||
let uri = uri.clone();
|
||||
crate::core::Error::SessionResourceNotFound { kind, uri }.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_mut_tree(&self, uri: &lsp::Url) -> anyhow::Result<RefMut<'_, lsp::Url, Mutex<tree_sitter::Tree>>> {
|
||||
self.document_trees.get_mut(uri).ok_or_else(|| {
|
||||
let kind = SessionResourceKind::Tree;
|
||||
let uri = uri.clone();
|
||||
crate::core::Error::SessionResourceNotFound { kind, uri }.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_channel_syntax() -> anyhow::Result<web_sys::HtmlTextAreaElement> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let element_id = "channel-syntax";
|
||||
let channel_syntax = web_sys::window()
|
||||
.ok_or(anyhow!("failed to get window"))?
|
||||
.document()
|
||||
.ok_or(anyhow!("failed to get document"))?
|
||||
.get_element_by_id(element_id)
|
||||
.ok_or(anyhow!("failed to get channel-syntax element"))?
|
||||
.unchecked_into();
|
||||
Ok(channel_syntax)
|
||||
}
|
||||
}
|
20
crates/server/src/core/syntax.rs
Normal file
20
crates/server/src/core/syntax.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use crate::core::session::Session;
|
||||
|
||||
pub(crate) fn update_channel(tree: Option<&tree_sitter::Tree>) {
|
||||
// assume errors; use red
|
||||
let mut color = "rgb(255, 87, 51)";
|
||||
if let Ok(channel_syntax) = Session::get_channel_syntax() {
|
||||
if let Some(tree) = tree {
|
||||
let sexp = crate::format_sexp(tree.root_node().to_sexp());
|
||||
channel_syntax.set_value(&sexp);
|
||||
if !tree.root_node().has_error() {
|
||||
// no errors; use green
|
||||
color = "rgb(218, 247, 166)";
|
||||
}
|
||||
}
|
||||
channel_syntax
|
||||
.style()
|
||||
.set_property("background-color", color)
|
||||
.expect("failed to set style");
|
||||
}
|
||||
}
|
17
crates/server/src/core/text.rs
Normal file
17
crates/server/src/core/text.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
pub struct Text {
|
||||
pub content: ropey::Rope,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new(text: impl AsRef<str>) -> anyhow::Result<Self> {
|
||||
let text = text.as_ref();
|
||||
let content = ropey::Rope::from_str(text);
|
||||
Ok(Text { content })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::core::Document> for Text {
|
||||
fn from(value: crate::core::Document) -> Self {
|
||||
value.text()
|
||||
}
|
||||
}
|
41
crates/server/src/handler.rs
Normal file
41
crates/server/src/handler.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
pub mod text_document {
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn did_open(
|
||||
session: Arc<crate::core::Session>,
|
||||
params: lsp::DidOpenTextDocumentParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let uri = params.text_document.uri.clone();
|
||||
|
||||
if let Some(document) = crate::core::Document::open(session.clone(), params).await? {
|
||||
session.insert_document(uri.clone(), document)?;
|
||||
} else {
|
||||
log::warn!("'textDocument/didOpen' failed :: uri: {:#?}", uri);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn did_change(
|
||||
session: Arc<crate::core::Session>,
|
||||
params: lsp::DidChangeTextDocumentParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let uri = ¶ms.text_document.uri;
|
||||
let mut text = session.get_mut_text(uri).await?;
|
||||
*text = crate::core::Text::new(params.content_changes[0].text.clone())?;
|
||||
crate::core::Document::change(session.clone(), uri, &text.content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn did_close(
|
||||
session: Arc<crate::core::Session>,
|
||||
params: lsp::DidCloseTextDocumentParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let uri = params.text_document.uri;
|
||||
session.remove_document(&uri)?;
|
||||
let diagnostics = Default::default();
|
||||
let version = Default::default();
|
||||
session.client()?.publish_diagnostics(uri, diagnostics, version).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
66
crates/server/src/lib.rs
Normal file
66
crates/server/src/lib.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
#![deny(clippy::all)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
mod core;
|
||||
pub mod handler;
|
||||
mod server;
|
||||
|
||||
pub use server::*;
|
||||
|
||||
pub(crate) fn format_sexp(sexp: impl AsRef<str>) -> String {
|
||||
format_sexp_indented(sexp, 0)
|
||||
}
|
||||
|
||||
fn format_sexp_indented(sexp: impl AsRef<str>, initial_indent_level: u32) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let sexp = sexp.as_ref();
|
||||
let mut formatted = String::new();
|
||||
let mut indent_level = initial_indent_level;
|
||||
let mut has_field = false;
|
||||
let mut s_iter = sexp.split(|c| c == ' ' || c == ')');
|
||||
while let Some(s) = s_iter.next() {
|
||||
if s.is_empty() {
|
||||
// ")"
|
||||
indent_level -= 1;
|
||||
write!(formatted, ")").unwrap();
|
||||
} else if s.starts_with('(') {
|
||||
if has_field {
|
||||
has_field = false;
|
||||
} else {
|
||||
if indent_level > 0 {
|
||||
writeln!(formatted, "").unwrap();
|
||||
for _ in 0 .. indent_level {
|
||||
write!(formatted, " ").unwrap();
|
||||
}
|
||||
}
|
||||
indent_level += 1;
|
||||
}
|
||||
|
||||
// "(node_name"
|
||||
write!(formatted, "{}", s).unwrap();
|
||||
|
||||
let mut c_iter = s.chars();
|
||||
c_iter.next();
|
||||
match c_iter.next() {
|
||||
Some('M') | Some('U') => {
|
||||
// "(MISSING node_name" or "(UNEXPECTED 'x'"
|
||||
let s = s_iter.next().unwrap();
|
||||
write!(formatted, " {}", s).unwrap();
|
||||
},
|
||||
Some(_) | None => {},
|
||||
}
|
||||
} else if s.ends_with(':') {
|
||||
// "field:"
|
||||
writeln!(formatted, "").unwrap();
|
||||
for _ in 0 .. indent_level {
|
||||
write!(formatted, " ").unwrap();
|
||||
}
|
||||
write!(formatted, "{} ", s).unwrap();
|
||||
has_field = true;
|
||||
indent_level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
formatted
|
||||
}
|
126
crates/server/src/server.rs
Normal file
126
crates/server/src/server.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use std::sync::Arc;
|
||||
use tower_lsp::{jsonrpc, lsp_types::*, LanguageServer};
|
||||
|
||||
pub fn capabilities() -> lsp::ServerCapabilities {
|
||||
let document_symbol_provider = Some(lsp::OneOf::Left(true));
|
||||
|
||||
let text_document_sync = {
|
||||
let options = lsp::TextDocumentSyncOptions {
|
||||
open_close: Some(true),
|
||||
change: Some(lsp::TextDocumentSyncKind::FULL),
|
||||
..Default::default()
|
||||
};
|
||||
Some(lsp::TextDocumentSyncCapability::Options(options))
|
||||
};
|
||||
|
||||
lsp::ServerCapabilities {
|
||||
text_document_sync,
|
||||
document_symbol_provider,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
pub client: tower_lsp::Client,
|
||||
pub session: Arc<crate::core::Session>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(client: tower_lsp::Client, language: tree_sitter::Language) -> Self {
|
||||
let session = crate::core::Session::new(Some(client.clone()), language);
|
||||
Server { client, session }
|
||||
}
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Server {
|
||||
async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {
|
||||
web_sys::console::log_1(&"server::initialize".into());
|
||||
*self.session.client_capabilities.write().await = Some(params.capabilities);
|
||||
let capabilities = capabilities();
|
||||
Ok(InitializeResult {
|
||||
capabilities,
|
||||
..InitializeResult::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialized(&self, _: lsp::InitializedParams) {
|
||||
web_sys::console::log_1(&"server::initialized".into());
|
||||
let typ = lsp::MessageType::INFO;
|
||||
let message = "demo language server initialized!";
|
||||
self.client.log_message(typ, message).await;
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> jsonrpc::Result<()> {
|
||||
web_sys::console::log_1(&"server::shutdown".into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: for some reason this doesn't trigger
|
||||
async fn did_open(&self, params: lsp::DidOpenTextDocumentParams) {
|
||||
web_sys::console::log_1(&"server::did_open".into());
|
||||
|
||||
let typ = lsp::MessageType::INFO;
|
||||
let message = format!("opened document: {}", params.text_document.uri.as_str());
|
||||
self.client.log_message(typ, message).await;
|
||||
|
||||
let session = self.session.clone();
|
||||
crate::handler::text_document::did_open(session, params).await.unwrap();
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: lsp::DidChangeTextDocumentParams) {
|
||||
web_sys::console::log_1(&"server::did_change".into());
|
||||
let session = self.session.clone();
|
||||
crate::handler::text_document::did_change(session, params)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn document_symbol(
|
||||
&self,
|
||||
params: lsp::DocumentSymbolParams,
|
||||
) -> jsonrpc::Result<Option<lsp::DocumentSymbolResponse>> {
|
||||
web_sys::console::log_1(&"server::document_symbol".into());
|
||||
let _params = params;
|
||||
let _session = self.session.clone();
|
||||
let uri = lsp::Url::parse("inmemory://model.fs").expect("failed to parse url");
|
||||
Ok(Some(lsp::DocumentSymbolResponse::Flat(vec![
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: "foo".into(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
tags: Default::default(),
|
||||
deprecated: Default::default(),
|
||||
location: lsp::Location {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 1)),
|
||||
uri: uri.clone(),
|
||||
},
|
||||
container_name: Default::default(),
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: "bar".into(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
tags: Default::default(),
|
||||
deprecated: Default::default(),
|
||||
location: lsp::Location {
|
||||
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(3, 1)),
|
||||
uri: uri.clone(),
|
||||
},
|
||||
container_name: Default::default(),
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: "baz".into(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
tags: Default::default(),
|
||||
deprecated: Default::default(),
|
||||
location: lsp::Location {
|
||||
range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(5, 1)),
|
||||
uri: uri.clone(),
|
||||
},
|
||||
container_name: Default::default(),
|
||||
},
|
||||
])))
|
||||
}
|
||||
}
|
10613
package-lock.json
generated
Normal file
10613
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
10
package.json
Normal file
10
package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/app"
|
||||
],
|
||||
"dependencies": {
|
||||
"tree-sitter-javascript": "^0.19.0",
|
||||
"web-tree-sitter": "https://gitpkg.now.sh/silvanshade/tree-sitter/lib/binding_web?web-tree-sitter-sys@v0.20.6"
|
||||
}
|
||||
}
|
1
packages/app/.prettierignore
Normal file
1
packages/app/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
dist
|
50
packages/app/assets/index.html
Normal file
50
packages/app/assets/index.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>
|
||||
<%= htmlWebpackPlugin.options.title %>
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1 id="title">browser-hosted editor and language server</h1>
|
||||
<p id="synopsis">
|
||||
This app demos an editor with language smarts (for JavaScript) by hosting the <a
|
||||
href="https://microsoft.github.io/monaco-editor/">Monaco</a> editor widget with a simple <a
|
||||
href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/">LSP</a>
|
||||
server implemented via <a href="https://github.com/ebkalderon/tower-lsp">tower-lsp</a> and <a
|
||||
href="https://github.com/tree-sitter/tree-sitter">tree-sitter</a>. Everything is compiled to WASM and run
|
||||
client-side directly in the browser; there are no separate sever-side processes or web workers used by the app
|
||||
code. (Monaco itself does use web workers, however).
|
||||
</p>
|
||||
<p id="features">
|
||||
features: ⇧⌘O (macos) or ⇧⌃O (windows) opens the symbol view; the <strong>syntax</strong> area shows the JavaScript syntax tree (green for valid; red for errors) parsed from the text <strong>editor</strong> area
|
||||
</p>
|
||||
<div id="cell-editor">
|
||||
<label for="editor">editor</label>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div id="cell-syntax">
|
||||
<label for="channel-syntax">syntax</label>
|
||||
<textarea id="channel-syntax" autocomplete="off" spellcheck="off" wrap="off" readonly></textarea>
|
||||
</div>
|
||||
<div id="cell-console">
|
||||
<label for="channel-console">console</label>
|
||||
<textarea id="channel-console" autocomplete="off" spellcheck="off" wrap="off" readonly></textarea>
|
||||
</div>
|
||||
<div id="cell-client">
|
||||
<label for="channel-client">message trace (client ⇒ server)</label>
|
||||
<textarea id="channel-client" autocomplete="off" spellcheck="off" wrap="off" readonly></textarea>
|
||||
</div>
|
||||
<div id="cell-server">
|
||||
<label for="channel-server">message trace (client ⇐ server)</label>
|
||||
<textarea id="channel-server" autocomplete="off" spellcheck="off" wrap="off" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
106
packages/app/assets/index.module.css
Normal file
106
packages/app/assets/index.module.css
Normal file
|
@ -0,0 +1,106 @@
|
|||
a {
|
||||
color: mediumslateblue;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: silver;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div[id=container] {
|
||||
display: grid;
|
||||
height: 90%;
|
||||
width: 75%;
|
||||
grid-template-rows: auto auto auto repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 2em;
|
||||
margin: auto;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1[id=title] {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
font-family: monospace;
|
||||
font-size: 2em;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p[id=synopsis] {
|
||||
color: lightgrey;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p[id=features] {
|
||||
color: lightgrey;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 3;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div[id=cell-editor] {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 4 / 6;
|
||||
}
|
||||
|
||||
[id=cell-editor] div[id=editor] {
|
||||
border: 1px solid black;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div[id=cell-syntax] {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 4 / 6;
|
||||
}
|
||||
|
||||
[id=container] label {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
div[id=cell-console] {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 6;
|
||||
}
|
||||
|
||||
div[id=cell-client] {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
div[id=cell-server] {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
div[id=container] textarea {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: borcder-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
}
|
1
packages/app/declarations.d.ts
vendored
Normal file
1
packages/app/declarations.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "*.module.css";
|
58
packages/app/package.json
Normal file
58
packages/app/package.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "monaco-lsp-streams",
|
||||
"description": "",
|
||||
"version": "0.0.0",
|
||||
"license": "Apache-2.0 WITH LLVM-exception",
|
||||
"author": {
|
||||
"name": "silvanshade",
|
||||
"email": "silvanshade@users.noreply.github.com",
|
||||
"url": "https://github.com/silvanshade"
|
||||
},
|
||||
"homepage": "https://github.com/silvanshade/monaco-lsp-streams#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/silvanshade/monaco-lsp-streams.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/silvanshade/monaco-lsp-streams/issues"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"format": "prettier --write '**/*.{js,json,ts,tsx,yml,yaml}'",
|
||||
"lint": "eslint 'src/**/*.{js,ts,tsx}' && prettier --check '**/*.{json,yml,yaml}'",
|
||||
"app": "webpack serve --open"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"esbuild-loader": "^2.19.0",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.6.2",
|
||||
"source-map-loader": "^4.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.3",
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debounce": "^1.2.1",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"monaco-editor-core": "^0.33.0",
|
||||
"monaco-languageclient": "^1.0.1",
|
||||
"vscode-languageserver-protocol": "^3.17.1"
|
||||
}
|
||||
}
|
97
packages/app/src/app.ts
Normal file
97
packages/app/src/app.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import debounce from "debounce";
|
||||
import * as monaco from "monaco-editor-core";
|
||||
import { MonacoToProtocolConverter } from "monaco-languageclient";
|
||||
import * as proto from "vscode-languageserver-protocol";
|
||||
|
||||
import Client from "./client";
|
||||
import { FromServer, IntoServer } from "./codec";
|
||||
import Language from "./language";
|
||||
import Server from "./server";
|
||||
|
||||
class Environment implements monaco.Environment {
|
||||
getWorkerUrl(moduleId: string, label: string) {
|
||||
if (label === "editorWorkerService") {
|
||||
return "./editor.worker.bundle.js";
|
||||
}
|
||||
throw new Error(`getWorkerUrl: unexpected ${JSON.stringify({ moduleId, label })}`);
|
||||
}
|
||||
}
|
||||
|
||||
const monacoToProtocol = new MonacoToProtocolConverter(monaco);
|
||||
|
||||
export default class App {
|
||||
readonly #window: Window & monaco.Window & typeof globalThis = self;
|
||||
|
||||
readonly #intoServer: IntoServer = new IntoServer();
|
||||
readonly #fromServer: FromServer = FromServer.create();
|
||||
|
||||
initializeMonaco(): void {
|
||||
this.#window.MonacoEnvironment = new Environment();
|
||||
}
|
||||
|
||||
createModel(client: Client): monaco.editor.ITextModel {
|
||||
const language = Language.initialize(client);
|
||||
|
||||
const value = `
|
||||
function foo() {
|
||||
}
|
||||
function bar() {
|
||||
}
|
||||
function baz() {
|
||||
}
|
||||
`.replace(/^\s*\n/gm, "");
|
||||
const id = language.id;
|
||||
const uri = monaco.Uri.parse("inmemory://demo.js");
|
||||
|
||||
const model = monaco.editor.createModel(value, id, uri);
|
||||
|
||||
model.onDidChangeContent(
|
||||
debounce(() => {
|
||||
const text = model.getValue();
|
||||
client.notify(proto.DidChangeTextDocumentNotification.type.method, {
|
||||
textDocument: {
|
||||
version: 0,
|
||||
uri: model.uri.toString(),
|
||||
},
|
||||
contentChanges: [
|
||||
{
|
||||
range: monacoToProtocol.asRange(model.getFullModelRange()),
|
||||
text,
|
||||
},
|
||||
],
|
||||
} as proto.DidChangeTextDocumentParams);
|
||||
}, 400),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
client.pushAfterInitializeHook(async () => {
|
||||
client.notify(proto.DidOpenTextDocumentNotification.type.method, {
|
||||
textDocument: {
|
||||
uri: model.uri.toString(),
|
||||
languageId: language.id,
|
||||
version: 0,
|
||||
text: model.getValue(),
|
||||
},
|
||||
} as proto.DidOpenTextDocumentParams);
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
createEditor(client: Client): void {
|
||||
const container = document.getElementById("editor")!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
this.initializeMonaco();
|
||||
const model = this.createModel(client);
|
||||
monaco.editor.create(container, {
|
||||
model,
|
||||
automaticLayout: true,
|
||||
});
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const client = new Client(this.#fromServer, this.#intoServer);
|
||||
const server = await Server.initialize(this.#intoServer, this.#fromServer);
|
||||
this.createEditor(client);
|
||||
await Promise.all([server.start(), client.start()]);
|
||||
}
|
||||
}
|
102
packages/app/src/client.ts
Normal file
102
packages/app/src/client.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import * as jsrpc from "json-rpc-2.0";
|
||||
import * as proto from "vscode-languageserver-protocol";
|
||||
|
||||
import { Bytes, FromServer, Headers, IntoServer } from "./codec";
|
||||
|
||||
const consoleChannel = document.getElementById("channel-console") as HTMLTextAreaElement;
|
||||
|
||||
class Codec {
|
||||
static encode(json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse): Uint8Array {
|
||||
const message = JSON.stringify(json);
|
||||
const delimited = Headers.add(message);
|
||||
return Bytes.encode(delimited);
|
||||
}
|
||||
|
||||
static decode<T>(data: Uint8Array): T {
|
||||
const delimited = Bytes.decode(data);
|
||||
const message = Headers.remove(delimited);
|
||||
return JSON.parse(message) as T;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
afterInitializedHooks: (() => Promise<void>)[] = [];
|
||||
#fromServer: FromServer;
|
||||
|
||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
||||
super(
|
||||
new jsrpc.JSONRPCServer(),
|
||||
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
||||
const encoded = Codec.encode(json);
|
||||
intoServer.enqueue(encoded);
|
||||
if (null != json.id) {
|
||||
const response = await fromServer.responses.get(json.id);
|
||||
this.client.receive(response as jsrpc.JSONRPCResponse);
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.#fromServer = fromServer;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async start(): Promise<void> {
|
||||
// process "window/logMessage": client <- server
|
||||
this.addMethod(proto.LogMessageNotification.type.method, (params) => {
|
||||
const { type, message } = params as { type: proto.MessageType; message: string };
|
||||
switch (type) {
|
||||
case proto.MessageType.Error: {
|
||||
consoleChannel.value += "[error] ";
|
||||
break;
|
||||
}
|
||||
case proto.MessageType.Warning: {
|
||||
consoleChannel.value += " [warn] ";
|
||||
break;
|
||||
}
|
||||
case proto.MessageType.Info: {
|
||||
consoleChannel.value += " [info] ";
|
||||
break;
|
||||
}
|
||||
case proto.MessageType.Log: {
|
||||
consoleChannel.value += " [log] ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
consoleChannel.value += message;
|
||||
consoleChannel.value += "\n";
|
||||
return;
|
||||
});
|
||||
|
||||
// request "initialize": client <-> server
|
||||
await (this.request(proto.InitializeRequest.type.method, {
|
||||
processId: null,
|
||||
clientInfo: {
|
||||
name: "demo-language-client",
|
||||
},
|
||||
capabilities: {},
|
||||
rootUri: null,
|
||||
} as proto.InitializeParams) as Promise<jsrpc.JSONRPCResponse>);
|
||||
|
||||
// notify "initialized": client --> server
|
||||
this.notify(proto.InitializedNotification.type.method, {});
|
||||
|
||||
await Promise.allSettled(this.afterInitializedHooks.map((f: () => Promise<void>) => f()));
|
||||
await Promise.allSettled([this.processNotifications(), this.processRequests()]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async processNotifications(): Promise<void> {
|
||||
for await (const notification of this.#fromServer.notifications) {
|
||||
await this.receiveAndSend(notification);
|
||||
}
|
||||
}
|
||||
|
||||
async processRequests(): Promise<void> {
|
||||
for await (const request of this.#fromServer.requests) {
|
||||
await this.receiveAndSend(request);
|
||||
}
|
||||
}
|
||||
|
||||
pushAfterInitializeHook(...hooks: (() => Promise<void>)[]): void {
|
||||
this.afterInitializedHooks.push(...hooks);
|
||||
}
|
||||
}
|
136
packages/app/src/codec.ts
Normal file
136
packages/app/src/codec.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
import * as vsrpc from "vscode-jsonrpc";
|
||||
|
||||
import Queue from "./queue";
|
||||
import Tracer from "./tracer";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export class Bytes {
|
||||
static encode(input: string): Uint8Array {
|
||||
return encoder.encode(input);
|
||||
}
|
||||
|
||||
static decode(input: Uint8Array): string {
|
||||
return decoder.decode(input);
|
||||
}
|
||||
}
|
||||
|
||||
export class Headers {
|
||||
static add(message: string): string {
|
||||
return `Content-Length: ${message.length}\r\n\r\n${message}`;
|
||||
}
|
||||
|
||||
static remove(delimited: string): string {
|
||||
return delimited.replace(/^Content-Length:\s*\d+\s*/, "");
|
||||
}
|
||||
}
|
||||
|
||||
export class PromiseMap<K, V extends { toString(): string }> {
|
||||
#map: Map<K, PromiseMap.type<V>> = new Map();
|
||||
|
||||
async get(key: K & { toString(): string }): Promise<V> {
|
||||
let initialized: PromiseMap.type<V>;
|
||||
if (!this.#map.has(key)) {
|
||||
initialized = this.#set(key);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
initialized = this.#map.get(key)!;
|
||||
}
|
||||
return initialized.promise;
|
||||
}
|
||||
|
||||
#set(key: K, value?: V): PromiseMap.type<V> {
|
||||
if (this.#map.has(key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return this.#map.get(key)!;
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
let partial: PromiseMap.partial<V> = {};
|
||||
partial.promise = new Promise<V>((resolve) => {
|
||||
partial.resolve = resolve;
|
||||
});
|
||||
const initialized = partial as PromiseMap.type<V>;
|
||||
if (null != value) {
|
||||
initialized.resolve(value);
|
||||
}
|
||||
this.#map.set(key, initialized);
|
||||
return initialized;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.#map.has(key);
|
||||
}
|
||||
|
||||
set(key: K & { toString(): string }, value: V): this {
|
||||
const initialized = this.#set(key, value);
|
||||
initialized.resolve(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#map.size;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PromiseMap {
|
||||
export type type<V> = { resolve: (item: V) => void; promise: Promise<V> };
|
||||
export type partial<V> = { resolve?: (item: V) => void; promise?: Promise<V> };
|
||||
}
|
||||
|
||||
// FIXME: tracing effiency
|
||||
export class IntoServer extends Queue<Uint8Array> implements AsyncGenerator<Uint8Array, never, void> {
|
||||
enqueue(item: Uint8Array): void {
|
||||
const delimited = decoder.decode(item);
|
||||
const message = Headers.remove(delimited);
|
||||
Tracer.client(message);
|
||||
super.enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FromServer extends WritableStream<Uint8Array> {
|
||||
readonly responses: { get(key: number | string): Promise<vsrpc.ResponseMessage> };
|
||||
readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void>;
|
||||
readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FromServer {
|
||||
export function create(): FromServer {
|
||||
return new StreamDemuxer();
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamDemuxer extends Queue<Uint8Array> {
|
||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> = new PromiseMap();
|
||||
readonly notifications: Queue<vsrpc.NotificationMessage> = new Queue<vsrpc.NotificationMessage>();
|
||||
readonly requests: Queue<vsrpc.RequestMessage> = new Queue<vsrpc.RequestMessage>();
|
||||
|
||||
readonly #start: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#start = this.start();
|
||||
}
|
||||
|
||||
private async start(): Promise<void> {
|
||||
for await (const bytes of this) {
|
||||
const delimited = Bytes.decode(bytes);
|
||||
const message = JSON.parse(Headers.remove(delimited)) as vsrpc.Message;
|
||||
Tracer.server(message);
|
||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
||||
this.responses.set(message.id, message);
|
||||
continue;
|
||||
}
|
||||
if (vsrpc.Message.isNotification(message)) {
|
||||
this.notifications.enqueue(message);
|
||||
continue;
|
||||
}
|
||||
if (vsrpc.Message.isRequest(message)) {
|
||||
this.requests.enqueue(message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
packages/app/src/index.ts
Normal file
6
packages/app/src/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../assets/index.module.css";
|
||||
|
||||
import App from "./app";
|
||||
|
||||
const app = new App();
|
||||
app.run().catch(console.error);
|
69
packages/app/src/language.ts
Normal file
69
packages/app/src/language.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
// import * as jsrpc from "json-rpc-2.0";
|
||||
import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from "monaco-languageclient";
|
||||
import * as monaco from "monaco-editor-core";
|
||||
import * as proto from "vscode-languageserver-protocol";
|
||||
|
||||
import Client from "./client";
|
||||
|
||||
const monacoToProtocol = new MonacoToProtocolConverter(monaco);
|
||||
const protocolToMonaco = new ProtocolToMonacoConverter(monaco);
|
||||
|
||||
let language: null | Language;
|
||||
|
||||
export default class Language implements monaco.languages.ILanguageExtensionPoint {
|
||||
readonly id: string;
|
||||
readonly aliases: string[];
|
||||
readonly extensions: string[];
|
||||
readonly mimetypes: string[];
|
||||
|
||||
private constructor(client: Client) {
|
||||
const { id, aliases, extensions, mimetypes } = Language.extensionPoint();
|
||||
this.id = id;
|
||||
this.aliases = aliases;
|
||||
this.extensions = extensions;
|
||||
this.mimetypes = mimetypes;
|
||||
this.registerLanguage(client);
|
||||
}
|
||||
|
||||
static extensionPoint(): monaco.languages.ILanguageExtensionPoint & {
|
||||
aliases: string[];
|
||||
extensions: string[];
|
||||
mimetypes: string[];
|
||||
} {
|
||||
const id = "javascript";
|
||||
const aliases = ["JavaScript", "javascript", "js"];
|
||||
const extensions = [".js", ".es6", ".mjs", ".cjs", ".pac"];
|
||||
const mimetypes = ["text/javascript"];
|
||||
return { id, extensions, aliases, mimetypes };
|
||||
}
|
||||
|
||||
private registerLanguage(client: Client): void {
|
||||
void client;
|
||||
monaco.languages.register(Language.extensionPoint());
|
||||
monaco.languages.registerDocumentSymbolProvider(this.id, {
|
||||
// eslint-disable-next-line
|
||||
async provideDocumentSymbols(model, token): Promise<monaco.languages.DocumentSymbol[]> {
|
||||
void token;
|
||||
const response = await (client.request(proto.DocumentSymbolRequest.type.method, {
|
||||
textDocument: monacoToProtocol.asTextDocumentIdentifier(model),
|
||||
} as proto.DocumentSymbolParams) as Promise<proto.SymbolInformation[]>);
|
||||
|
||||
const uri = model.uri.toString();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const result: monaco.languages.DocumentSymbol[] = protocolToMonaco.asSymbolInformations(response, uri);
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static initialize(client: Client): Language {
|
||||
if (null == language) {
|
||||
language = new Language(client);
|
||||
} else {
|
||||
console.warn("Language already initialized; ignoring");
|
||||
}
|
||||
return language;
|
||||
}
|
||||
}
|
103
packages/app/src/queue.ts
Normal file
103
packages/app/src/queue.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
export default class Queue<T> implements WritableStream<T>, AsyncGenerator<T, never, void> {
|
||||
readonly #promises: Promise<T>[] = [];
|
||||
readonly #resolvers: ((item: T) => void)[] = [];
|
||||
readonly #observers: ((item: T) => void)[] = [];
|
||||
|
||||
#closed = false;
|
||||
#locked = false;
|
||||
readonly #stream: WritableStream<T>;
|
||||
|
||||
static #__add<X>(promises: Promise<X>[], resolvers: ((item: X) => void)[]): void {
|
||||
promises.push(
|
||||
new Promise((resolve) => {
|
||||
resolvers.push(resolve);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static #__enqueue<X>(closed: boolean, promises: Promise<X>[], resolvers: ((item: X) => void)[], item: X): void {
|
||||
if (!closed) {
|
||||
if (!resolvers.length) Queue.#__add(promises, resolvers);
|
||||
const resolve = resolvers.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
resolve(item);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const closed = this.#closed;
|
||||
const promises = this.#promises;
|
||||
const resolvers = this.#resolvers;
|
||||
this.#stream = new WritableStream({
|
||||
write(item: T): void {
|
||||
Queue.#__enqueue(closed, promises, resolvers, item);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
#add(): void {
|
||||
return Queue.#__add(this.#promises, this.#resolvers);
|
||||
}
|
||||
|
||||
enqueue(item: T): void {
|
||||
return Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item);
|
||||
}
|
||||
|
||||
dequeue(): Promise<T> {
|
||||
if (!this.#promises.length) this.#add();
|
||||
const item = this.#promises.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
return item;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return !this.#promises.length;
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
return !!this.#resolvers.length;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.#promises.length - this.#resolvers.length;
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T, never>> {
|
||||
const done = false;
|
||||
const value = await this.dequeue();
|
||||
for (const observer of this.#observers) {
|
||||
observer(value);
|
||||
}
|
||||
return { done, value };
|
||||
}
|
||||
|
||||
return(): Promise<IteratorResult<T, never>> {
|
||||
return new Promise(() => {
|
||||
// empty
|
||||
});
|
||||
}
|
||||
|
||||
throw(err: Error): Promise<IteratorResult<T, never>> {
|
||||
return new Promise((_resolve, reject) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T, never, void> {
|
||||
return this;
|
||||
}
|
||||
|
||||
get locked(): boolean {
|
||||
return this.#stream.locked;
|
||||
}
|
||||
|
||||
abort(reason?: Error): Promise<void> {
|
||||
return this.#stream.abort(reason);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.#stream.close();
|
||||
}
|
||||
|
||||
getWriter(): WritableStreamDefaultWriter<T> {
|
||||
return this.#stream.getWriter();
|
||||
}
|
||||
}
|
31
packages/app/src/server.ts
Normal file
31
packages/app/src/server.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import init, { InitOutput, serve, ServerConfig } from "../assets/wasm/demo_lsp_browser";
|
||||
import { FromServer, IntoServer } from "./codec";
|
||||
|
||||
let server: null | Server;
|
||||
|
||||
export default class Server {
|
||||
readonly initOutput: InitOutput;
|
||||
readonly #intoServer: IntoServer;
|
||||
readonly #fromServer: FromServer;
|
||||
|
||||
private constructor(initOutput: InitOutput, intoServer: IntoServer, fromServer: FromServer) {
|
||||
this.initOutput = initOutput;
|
||||
this.#intoServer = intoServer;
|
||||
this.#fromServer = fromServer;
|
||||
}
|
||||
|
||||
static async initialize(intoServer: IntoServer, fromServer: FromServer): Promise<Server> {
|
||||
if (null == server) {
|
||||
const initOutput = await init();
|
||||
server = new Server(initOutput, intoServer, fromServer);
|
||||
} else {
|
||||
console.warn("Server already initialized; ignoring");
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const config = new ServerConfig(this.#intoServer, this.#fromServer);
|
||||
await serve(config);
|
||||
}
|
||||
}
|
17
packages/app/src/tracer.ts
Normal file
17
packages/app/src/tracer.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as proto from "vscode-languageserver-protocol";
|
||||
|
||||
const clientChannel = document.getElementById("channel-client") as HTMLTextAreaElement;
|
||||
const serverChannel = document.getElementById("channel-server") as HTMLTextAreaElement;
|
||||
|
||||
export default class Tracer {
|
||||
static client(message: string): void {
|
||||
clientChannel.value += message;
|
||||
clientChannel.value += "\n";
|
||||
}
|
||||
|
||||
static server(input: string | proto.Message): void {
|
||||
const message: string = typeof input === "string" ? input : JSON.stringify(input);
|
||||
serverChannel.value += message;
|
||||
serverChannel.value += "\n";
|
||||
}
|
||||
}
|
8
packages/app/tsconfig.json
Normal file
8
packages/app/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": []
|
||||
}
|
106
packages/app/webpack.config.js
Normal file
106
packages/app/webpack.config.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
// @ts-check
|
||||
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const webpack = require("webpack");
|
||||
|
||||
/** @type {import("webpack").Configuration & { devServer?: import("webpack-dev-server").Configuration } } */
|
||||
const config = {
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: "development",
|
||||
target: "web",
|
||||
entry: {
|
||||
app: "./src/index.ts",
|
||||
"editor.worker": "monaco-editor-core/esm/vs/editor/editor.worker.js",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vscode: require.resolve("monaco-languageclient/vscode-compatibility"),
|
||||
},
|
||||
extensions: [".ts", ".js", ".json", ".ttf"],
|
||||
fallback: {
|
||||
fs: false,
|
||||
child_process: false,
|
||||
net: false,
|
||||
crypto: false,
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
},
|
||||
output: {
|
||||
globalObject: "self",
|
||||
filename: "[name].bundle.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts?$/,
|
||||
loader: "esbuild-loader",
|
||||
options: {
|
||||
loader: "ts",
|
||||
target: "es2022",
|
||||
minify: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||
type: "asset/resource",
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProgressPlugin(),
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "../../node_modules/web-tree-sitter/tree-sitter.wasm",
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "assets/index.html",
|
||||
scriptLoading: "module",
|
||||
title: "tower-lsp web demo",
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: true,
|
||||
// sourceMap: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, "dist"),
|
||||
},
|
||||
compress: true,
|
||||
port: 9000,
|
||||
client: {
|
||||
progress: true,
|
||||
reconnect: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -8,7 +8,7 @@ comment_width = 100
|
|||
condense_wildcard_suffixes = true
|
||||
# control_brace_style = "AlwaysSameLine"
|
||||
# disable_all_formatting = false
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
empty_item_single_line = false
|
||||
# enum_discrim_align_threshold = 0
|
||||
error_on_line_overflow = true
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = ["tower-lsp/runtime-agnostic"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.21"
|
||||
js-sys = "0.3.57"
|
||||
tower-lsp = { version = "0.17.0", default-features = false }
|
||||
wasm-bindgen = "0.2.80"
|
||||
wasm-bindgen-futures = { version = "0.4.30", features = ["futures-core-03-stream"] }
|
||||
wasm-streams = "0.2.3"
|
||||
web-sys = { version = "0.3.57", features = [ "console", "ReadableStream", "WritableStream" ] }
|
||||
|
||||
[dev-dependencies]
|
|
@ -1,56 +0,0 @@
|
|||
#![cfg(web_sys_unstable_apis)]
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
use js_sys::Uint8Array;
|
||||
use tower_lsp::{jsonrpc, lsp_types::*, LanguageServer, LspService, Server};
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use wasm_bindgen_futures::stream::JsStream;
|
||||
|
||||
struct LspServer {}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for LspServer {
|
||||
async fn initialize(&self, _: InitializeParams) -> jsonrpc::Result<InitializeResult> {
|
||||
web_sys::console::log_1(&"server::initialize".into());
|
||||
Ok(InitializeResult {
|
||||
..InitializeResult::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> jsonrpc::Result<()> {
|
||||
web_sys::console::log_1(&"server::shutdown".into());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: we don't use web_sys::ReadableStream for input here because on the
|
||||
// browser side we need to use a ReadableByteStreamController to construct it
|
||||
// and so far only Chromium-based browsers support that functionality.
|
||||
|
||||
// NOTE: input needs to be an AsyncIterator<Uint8Array> specifically
|
||||
#[wasm_bindgen]
|
||||
pub async fn serve(input: js_sys::AsyncIterator, output: web_sys::WritableStream) -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
web_sys::console::log_1(&"server::serve".into());
|
||||
|
||||
let input = JsStream::from(input);
|
||||
let input = input
|
||||
.map_ok(|value| {
|
||||
value
|
||||
.dyn_into::<Uint8Array>()
|
||||
.expect("could not cast stream item to Uint8Array")
|
||||
.to_vec()
|
||||
})
|
||||
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
|
||||
.into_async_read();
|
||||
|
||||
let output = JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(output);
|
||||
let output = wasm_streams::WritableStream::from_raw(output);
|
||||
let output = output.try_into_async_write().map_err(|err| err.0)?;
|
||||
|
||||
let (service, messages) = LspService::new(|_client| LspServer {});
|
||||
Server::new(input, output, messages).serve(service).await;
|
||||
|
||||
Ok(())
|
||||
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "ES2022"],
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"newLine": "lf",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
// "sourceMap": true,
|
||||
},
|
||||
"exclude": ["."],
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
},
|
||||
},
|
||||
}
|
Loading…
Reference in a new issue