[mod] improve sock err handling
This commit is contained in:
parent
9c805c2a21
commit
b7d0d8e03d
11 changed files with 108 additions and 71 deletions
31
README.md
31
README.md
|
@ -1,18 +1,19 @@
|
||||||
Work in progress.
|
Work in progress.
|
||||||
|
|
||||||
An attempt to write a [Language Server Protocol](https://en.wikipedia.org/wiki/Language_Server_Protocol) server using XQuery 4.0 and the [BaseX websocket](https://docs.basex.org/main/WebSockets) feature.
|
An attempt to write a [Language Server Protocol](https://en.wikipedia.org/wiki/Language_Server_Protocol) server for and in XQuery 4.0. It uses the [BaseX websocket](https://docs.basex.org/main/WebSockets) feature.
|
||||||
|
|
||||||
|
# Demo
|
||||||
|
1. Requires `Docker` or `Podman` on the local machine.
|
||||||
|
2. Clone this repo
|
||||||
|
3. Run `docker compose up -d` The LSP server shall now running on port 3000 (edit `compose.yaml` to change port)
|
||||||
|
4. In a browser navigate to `http://localhost:3000/static/clients/codemirror/` to see the CodeMirror 6 demo.
|
||||||
|
|
||||||
|
In principle the LSP can be used in any environment that can make use of a websocket accessed LSP.
|
||||||
## LSP Server
|
## LSP Server
|
||||||
* `webapp/lsp` The LSP implementation in XQuery using WebSockets for transport and the [JSON-RPC](https://www.jsonrpc.org/specification) 2.0 API for data format.
|
* `webapp/lsp` The LSP implementation in XQuery using WebSockets for transport and the [JSON-RPC](https://www.jsonrpc.org/specification) 2.0 API for data format.
|
||||||
### Dev notes
|
|
||||||
State is held in websocket attributes.
|
|
||||||
|
|
||||||
|Name|Use|
|
|
||||||
|----|---|
|
|
||||||
|initized|set true after client sends intialized messag|
|
|
||||||
|files|A map whose keys are open uris, values are maps (doctype-> attribute name where doctype is stored| |
|
|
||||||
|file-{uuid}|name of websocket attribute with textDocument|
|
|
||||||
|parse-{uuid}|name of websocket attribute with parse tree XML|
|
|
||||||
## Sample html clients
|
## Sample html clients
|
||||||
|
A couple of samples using common browser based code editor components are available. The `CodeMirror 6` sample is the most functional.
|
||||||
### Using `CodeMirror6` https://codemirror.net/
|
### Using `CodeMirror6` https://codemirror.net/
|
||||||
* `webapp/static/codemirror` A test html page using the [CodeMirror6 editor](https://codemirror.net/) that connects to the BaseX LSP instance
|
* `webapp/static/codemirror` A test html page using the [CodeMirror6 editor](https://codemirror.net/) that connects to the BaseX LSP instance
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ State is held in websocket attributes.
|
||||||
|
|
||||||
### Using `Ace Editor`
|
### Using `Ace Editor`
|
||||||
* `webapp/static/ace` A test html page using the [Ace editor](https://ace.c9.io/) that connects to the BaseX LSP instance
|
* `webapp/static/ace` A test html page using the [Ace editor](https://ace.c9.io/) that connects to the BaseX LSP instance
|
||||||
|
@TODO fix
|
||||||
#### Uses
|
#### Uses
|
||||||
* https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/
|
* https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/
|
||||||
|
|
||||||
|
@ -38,8 +39,18 @@ Or `node --max-old-space-size=8192 node_modules/webpack-dev-serve
|
||||||
r/bin/webpack-dev-server.js`
|
r/bin/webpack-dev-server.js`
|
||||||
|
|
||||||
### Monaco
|
### Monaco
|
||||||
|
@TODO create
|
||||||
https://socket.dev/npm/package/monaco-editor
|
https://socket.dev/npm/package/monaco-editor
|
||||||
|
|
||||||
|
### Dev notes
|
||||||
|
State is held in [websocket attributes](https://docs.basex.org/main/WebSocket_Functions#websocket_attributes).
|
||||||
|
|
||||||
|
|Name|Use|
|
||||||
|
|----|---|
|
||||||
|
|initialized|set true after client sends initialized messag|
|
||||||
|
|files|A map whose keys are open uris, values are maps (doctype-> attribute name where doctype is stored| |
|
||||||
|
|file-{uuid}|name of websocket attribute with textDocument|
|
||||||
|
|parse-{uuid}|name of websocket attribute with parse tree XML|
|
||||||
## Related links
|
## Related links
|
||||||
java -cp org.eclipse.lemminx-uber.jar org.eclipse.lemminx.XMLServerSocketLauncher`
|
java -cp org.eclipse.lemminx-uber.jar org.eclipse.lemminx.XMLServerSocketLauncher`
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState } from '@codemirror/state';
|
||||||
import { openSearchPanel, highlightSelectionMatches,searchKeymap } from '@codemirror/search';
|
import { openSearchPanel, highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
||||||
import {openLintPanel, lintGutter, lintKeymap,linter,setDiagnostics,} from "@codemirror/lint"
|
import { openLintPanel, lintGutter, lintKeymap, linter, setDiagnostics, } from "@codemirror/lint"
|
||||||
import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||||
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||||
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, EditorView } from '@codemirror/view';
|
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, EditorView } from '@codemirror/view';
|
||||||
import {markdown} from "@codemirror/lang-markdown";
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
// Theme
|
// Theme
|
||||||
import { oneDark } from "@codemirror/theme-one-dark";
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
import { LSPClient, LSPPlugin,languageServerSupport } from "@codemirror/lsp-client";
|
import { LSPClient, LSPPlugin, languageServerSupport } from "@codemirror/lsp-client";
|
||||||
// Language
|
// Language
|
||||||
import { xml } from "@codemirror/lang-xml";
|
import { xml } from "@codemirror/lang-xml";
|
||||||
|
|
||||||
// no error checking
|
// no error checking
|
||||||
function simpleWebSocketTransport(uri) {
|
function simpleWebSocketTransport(uri) {
|
||||||
let handlers = [];
|
let handlers = [];
|
||||||
let sock = new WebSocket(uri);
|
return new Promise(function (resolve, reject) {
|
||||||
sock.addEventListener("close", (event) => { alert("sock gone!")})
|
let sock = new WebSocket(uri);
|
||||||
sock.addEventListener("error", (event) => { alert("sock error!")})
|
sock.onmessage = e => { for (let h of handlers) h(e.data.toString()); };
|
||||||
sock.onmessage = e => { for (let h of handlers) h(e.data.toString()); };
|
sock.onerror = e => reject(e);
|
||||||
return new Promise(resolve => {
|
|
||||||
sock.onopen = () => resolve({
|
sock.onopen = () => resolve({
|
||||||
|
socket: sock,
|
||||||
send: (message) => sock.send(message),
|
send: (message) => sock.send(message),
|
||||||
subscribe: (handler) => handlers.push(handler),
|
subscribe: (handler) => handlers.push(handler),
|
||||||
unsubscribe: (handler) => handlers = handlers.filter(h => h != handler)
|
unsubscribe: (handler) => handlers = handlers.filter(h => h != handler)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseExts = [
|
const baseExts = [
|
||||||
|
@ -62,7 +62,7 @@ const baseExts = [
|
||||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
||||||
];
|
];
|
||||||
|
|
||||||
function createEditorState(initialContents, extensions,options = {}) {
|
function createEditorState(initialContents, extensions, options = {}) {
|
||||||
|
|
||||||
if (options.oneDark)
|
if (options.oneDark)
|
||||||
extensions.push(oneDark);
|
extensions.push(oneDark);
|
||||||
|
@ -78,8 +78,8 @@ function createEditorView(state, parent) {
|
||||||
}
|
}
|
||||||
class xqLinter {
|
class xqLinter {
|
||||||
constructor(parameters) {
|
constructor(parameters) {
|
||||||
this.fred=parameters;
|
this.fred = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createEditorState, createEditorView, openSearchPanel,openLintPanel, languageServerSupport, baseExts , simpleWebSocketTransport,linter, LSPPlugin,setDiagnostics,xqLinter,LSPClient};
|
export { createEditorState, createEditorView, openSearchPanel, openLintPanel, languageServerSupport, baseExts, simpleWebSocketTransport, linter, LSPPlugin, setDiagnostics, xqLinter, LSPClient };
|
|
@ -1 +1 @@
|
||||||
{"cells":[{"kind":2,"language":"xquery","value":"(:<:)\r\n\r\nimport module namespace docs=\"lsp/docs\" at \"/srv/basex/webapp/lsp/docs.xqm\";"},{"kind":2,"language":"xquery","value":"ws:ids()"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"textDocument\")\r\nreturn $t"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"parse\")\r\nreturn $t/self::ERROR"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"textDocument\")\r\nreturn $t"}]}
|
{"cells":[{"kind":2,"language":"xquery","value":"(:<:)\r\n\r\nimport module namespace docs=\"lsp/docs\" at \"/srv/basex/webapp/lsp/docs.xqm\";"},{"kind":2,"language":"xquery","value":"2+2"},{"kind":2,"language":"xquery","value":"ws:ids()"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"textDocument\")\r\nreturn $t"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"parse\")\r\nreturn $t/self::ERROR"},{"kind":2,"language":"xquery","value":"let $sock:=foot(ws:ids())\r\nlet $f:=docs:list($sock)\r\nlet $t:=docs:get($sock,$f,\"textDocument\")\r\nreturn $t"}]}
|
|
@ -51,3 +51,14 @@ declare %unit:test function test:line() {
|
||||||
let $p:= pos:lineAt($nl,18752)=>trace("AA")
|
let $p:= pos:lineAt($nl,18752)=>trace("AA")
|
||||||
return unit:assert-equals($p,518)
|
return unit:assert-equals($p,518)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(:~ convert position. :)
|
||||||
|
declare %unit:test function test:apply-change() {
|
||||||
|
let $text:=`11111111,
|
||||||
|
22222222,
|
||||||
|
33333333`
|
||||||
|
let $ch:= {"text":`11111111,
|
||||||
|
A22222222,
|
||||||
|
33333333`};
|
||||||
|
pos:apply-change($text,$ch)
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
{
|
{
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"textDocumentSync": 2,
|
"textDocumentSync": 2,
|
||||||
"completionProvider": {
|
"completionProvider": {
|
||||||
"resolveProvider": false,
|
"resolveProvider": false,
|
||||||
"triggerCharacters": [ "\"", ":" ],
|
"triggerCharacters": [
|
||||||
"documentSelector": [{ "language": "xquery" }]
|
"\"",
|
||||||
},
|
":"
|
||||||
"hoverProvider": true,
|
],
|
||||||
"documentSymbolProvider": false,
|
"documentSelector": [
|
||||||
"documentRangeFormattingProvider": false,
|
{
|
||||||
"colorProvider": {},
|
"language": "xquery"
|
||||||
"foldingRangeProvider": false,
|
|
||||||
"selectionRangeProvider": false,
|
|
||||||
"documentLinkProvider": {},
|
|
||||||
"serverInfo": {
|
|
||||||
"name": "XQuery 4.0b Language Server",
|
|
||||||
"version": "0.0.1"
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hoverProvider": false,
|
||||||
|
"documentSymbolProvider": false,
|
||||||
|
"documentRangeFormattingProvider": false,
|
||||||
|
"colorProvider": false,
|
||||||
|
"foldingRangeProvider": false,
|
||||||
|
"selectionRangeProvider": false,
|
||||||
|
"documentLinkProvider": {},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "XQuery 4.0b Language Server",
|
||||||
|
"version": "0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
7
webapp/lsp/etc/format-defaults.json
Normal file
7
webapp/lsp/etc/format-defaults.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"tabSize": 2,
|
||||||
|
"insertSpaces": true,
|
||||||
|
"trimTrailingWhitespace": true,
|
||||||
|
"insertFinalNewline": true,
|
||||||
|
"trimFinalNewlines": true
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ positions in text
|
||||||
:)
|
:)
|
||||||
module namespace pos="lsp/position";
|
module namespace pos="lsp/position";
|
||||||
|
|
||||||
|
(:~ json numbers :)
|
||||||
declare type pos:num as (xs:integer|xs:double);
|
declare type pos:num as (xs:integer|xs:double);
|
||||||
|
|
||||||
(:~
|
(:~
|
||||||
|
@ -89,7 +90,7 @@ as xs:integer
|
||||||
return $s?max
|
return $s?max
|
||||||
};
|
};
|
||||||
|
|
||||||
(:~ position text for display :)
|
(:~ format position for text display :)
|
||||||
declare function pos:ln-col($pos as pos:Position,$offset as xs:integer:=1)
|
declare function pos:ln-col($pos as pos:Position,$offset as xs:integer:=1)
|
||||||
{
|
{
|
||||||
`Ln { $pos?line + $offset}, Col { $pos?character + $offset}`
|
`Ln { $pos?line + $offset}, Col { $pos?character + $offset}`
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<form class="d-flex">
|
<form class="d-flex">
|
||||||
<span id="state">🔴</span>
|
<span id="state">🔴</span>
|
||||||
<input id="iServer" type="text" value="ws://localhost:3000/ws/lsp" style="width:25em" />
|
<input id="iServer" type="text" value="ws://localhost:3000/ws/lsp2" style="width:25em" />
|
||||||
<button id="connect">connect</button>
|
<button id="connect">connect</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -45,6 +45,7 @@
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row " style="overflow:hidden">
|
<div class="row " style="overflow:hidden">
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
<div id="msg"> </div>
|
||||||
<select id="load">
|
<select id="load">
|
||||||
<option selected value="">load..</option>
|
<option selected value="">load..</option>
|
||||||
<optgroup label="XQuery3">
|
<optgroup label="XQuery3">
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
<label for="symbols">Symbols:</label><select id="symbols" disabled="disabled"></select>
|
<label for="symbols">Symbols:</label><select id="symbols" disabled="disabled"></select>
|
||||||
<ul id="msg" style="overflow: scroll;">
|
<ul id="traffic" style="overflow: scroll;">
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31375,18 +31375,18 @@ ${text}</tr>
|
||||||
// no error checking
|
// no error checking
|
||||||
function simpleWebSocketTransport(uri) {
|
function simpleWebSocketTransport(uri) {
|
||||||
let handlers = [];
|
let handlers = [];
|
||||||
let sock = new WebSocket(uri);
|
return new Promise(function (resolve, reject) {
|
||||||
sock.addEventListener("close", (event) => { alert("sock gone!");});
|
let sock = new WebSocket(uri);
|
||||||
sock.addEventListener("error", (event) => { alert("sock error!");});
|
sock.onmessage = e => { for (let h of handlers) h(e.data.toString()); };
|
||||||
sock.onmessage = e => { for (let h of handlers) h(e.data.toString()); };
|
sock.onerror = e => reject(e);
|
||||||
return new Promise(resolve => {
|
|
||||||
sock.onopen = () => resolve({
|
sock.onopen = () => resolve({
|
||||||
|
socket: sock,
|
||||||
send: (message) => sock.send(message),
|
send: (message) => sock.send(message),
|
||||||
subscribe: (handler) => handlers.push(handler),
|
subscribe: (handler) => handlers.push(handler),
|
||||||
unsubscribe: (handler) => handlers = handlers.filter(h => h != handler)
|
unsubscribe: (handler) => handlers = handlers.filter(h => h != handler)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
const baseExts = [
|
const baseExts = [
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
|
@ -31421,7 +31421,7 @@ ${text}</tr>
|
||||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
||||||
];
|
];
|
||||||
|
|
||||||
function createEditorState(initialContents, extensions,options = {}) {
|
function createEditorState(initialContents, extensions, options = {}) {
|
||||||
|
|
||||||
if (options.oneDark)
|
if (options.oneDark)
|
||||||
extensions.push(oneDark);
|
extensions.push(oneDark);
|
||||||
|
@ -31437,7 +31437,7 @@ ${text}</tr>
|
||||||
}
|
}
|
||||||
class xqLinter {
|
class xqLinter {
|
||||||
constructor(parameters) {
|
constructor(parameters) {
|
||||||
this.fred=parameters;
|
this.fred = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,9 @@
|
||||||
|
const view = lsp.createEditorView(undefined, document.getElementById("editor"));
|
||||||
|
let doc = `3+1`;
|
||||||
|
var client;
|
||||||
|
var extLint;
|
||||||
|
|
||||||
|
function $(id){return document.getElementById(id)};
|
||||||
// Load saved content from localStorage when the page loads
|
// Load saved content from localStorage when the page loads
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const savedText = localStorage.getItem('code');
|
const savedText = localStorage.getItem('code');
|
||||||
|
@ -8,14 +13,6 @@ window.addEventListener('load', () => {
|
||||||
view.setState(lsp.createEditorState(doc, lsp.baseExts));
|
view.setState(lsp.createEditorState(doc, lsp.baseExts));
|
||||||
connect();
|
connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const view = lsp.createEditorView(undefined, document.getElementById("editor"));
|
|
||||||
let doc = `3+1`;
|
|
||||||
var client;
|
|
||||||
var extLint;
|
|
||||||
|
|
||||||
|
|
||||||
// Save content to localStorage when the page is about to unload
|
// Save content to localStorage when the page is about to unload
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
const doc = view.state.doc.toString();
|
const doc = view.state.doc.toString();
|
||||||
|
@ -68,9 +65,13 @@ function connect() {
|
||||||
lsp.simpleWebSocketTransport(server)
|
lsp.simpleWebSocketTransport(server)
|
||||||
.then(transport => {
|
.then(transport => {
|
||||||
transport.subscribe(incoming);
|
transport.subscribe(incoming);
|
||||||
|
transport.socket.onclose=(event) => $("msg").innerText="sock gone!";
|
||||||
|
transport.socket.oneror=(event) => $("msg").innerText="sock error!";
|
||||||
client = new lsp.LSPClient().connect(transport);
|
client = new lsp.LSPClient().connect(transport);
|
||||||
let extLsp=lsp.languageServerSupport(client,file,"xquery");
|
let extLsp=lsp.languageServerSupport(client,file,"xquery");
|
||||||
extLint=lsp.linter(null,{autoPanel:true});
|
extLint=lsp.linter(
|
||||||
|
view=>{ client.sync();return [];},
|
||||||
|
{autoPanel:true});
|
||||||
const doc = view.state.doc.toString();
|
const doc = view.state.doc.toString();
|
||||||
const state = lsp.createEditorState(doc, [...lsp.baseExts, extLsp,extLint]);
|
const state = lsp.createEditorState(doc, [...lsp.baseExts, extLsp,extLint]);
|
||||||
view.setState(state);
|
view.setState(state);
|
||||||
|
@ -96,10 +97,8 @@ function log(rpc){
|
||||||
if(rpc.id) return
|
if(rpc.id) return
|
||||||
const text=rpc.method;
|
const text=rpc.method;
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
|
|
||||||
li.appendChild(document.createTextNode(text));
|
li.appendChild(document.createTextNode(text));
|
||||||
const ol=document.getElementById("msg");
|
$("traffic").insertBefore(li,$("traffic").firstChild)
|
||||||
ol.insertBefore(li,ol.firstChild)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function diags(params){
|
function diags(params){
|
||||||
|
|
Loading…
Add table
Reference in a new issue