[mod] additions

This commit is contained in:
Andy Bunce 2025-07-31 15:02:27 +01:00
parent c59edb71a2
commit 2f54b3370e
19 changed files with 40083 additions and 16 deletions

3
.gitignore vendored
View file

@ -1 +1,2 @@
data/
data/
node_modules/

View file

@ -1,7 +1,10 @@
An attempt to write a language protocol server using BaseX features...
Work in progress.
* `webapp/lsp` A BaseX LSP application with a WebSocket Jsonrpc API
* `webapp/static/ace` A html page using an Ace editor that connects to a BaseX LSP instance
An attempt to write a [Language Server Protocol](https://en.wikipedia.org/wiki/Language_Server_Protocol) server using the BaseX websocket feature.
* `webapp/lsp` The implementation using the BaseX WebSocket feature and the Jsonrpc API
* `webapp/static/codemirror` A test html page using the Codemirror6 editor that connects to the BaseX LSP instance
* `webapp/static/ace` A test html page using the Ace editor that connects to the BaseX LSP instance
## Uses
* https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/

View file

@ -10,6 +10,7 @@ services:
volumes:
- ./data:/srv/basex/data
- ./webapp/static/ace:/srv/basex/webapp/static/ace
- ./webapp/static/codemirror:/srv/basex/webapp/static/codemirror
- ./webapp/lsp:/srv/basex/webapp/lsp
# - ./jars:/srv/basex/lib/custom

1299
grammar/XQuery-40.ebnf Normal file

File diff suppressed because it is too large Load diff

3
grammar/test.xq Normal file
View file

@ -0,0 +1,3 @@
import module namespace p="xq4" at "C:\Users\mrwhe\git\quodatum\basex-lsp\webapp\lsp\xq4.xqm";
let $t:=fetch:text("https://raw.githubusercontent.com/dnovatchev/Articles/refs/heads/main/Generators/Code/generator.xq")=>lazy:cache()
return p:parse-Module($t)

1244
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"dependencies": {
"@codemirror/commands": "^6.1.2",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lsp-client": "^6.0.0",
"@codemirror/search": "^6.2.3",
"@codemirror/theme-one-dark": "^6.1.0",
"@codemirror/view": "^6.6.0",
"minify": "^9.1.0",
"rollup": "4",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^12.1.4",
"rollup-plugin-tla": "^0.0.2"
},
"scripts": {
"build": "rollup src/index.js -m true -f iife -o dist/cm6.bundle.js -p node-resolve,tla --output.name cm6",
"build-cm": "rollup src/lsp.js -m true -f iife -o webapp/static/codemirror/lsp.bundle.js -p node-resolve,tla --output.name lsp",
"min-cm": "cd webapp/static/codemirror && npx minify lsp.bundle.js > lsp.bundle.min.js ",
"min": "cd dist && npx minify cm6.bundle.js > cm6.bundle.min.js && npx minify lsp.bundle.js > lsp.bundle.min.js"
}
}

14
rollup.config.js Normal file
View file

@ -0,0 +1,14 @@
import tla from 'rollup-plugin-tla';
import nodeResolve from 'rollup-node-resolve';
export default defineConfig({
input: 'src/editor.js',
output: {
format: 'iife',
dir: './dist',
file:"acm6.bundle.js",
name: `__Expose`,
sourcemap: true,
},
plugins: [ nodeResolve(),tla()],
});

79
src/lsp.js Normal file
View file

@ -0,0 +1,79 @@
import { EditorState } from '@codemirror/state';
import { openSearchPanel, highlightSelectionMatches,searchKeymap } from '@codemirror/search';
import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands';
import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, EditorView } from '@codemirror/view';
// Theme
import { oneDark } from "@codemirror/theme-one-dark";
import { LSPClient, languageServerSupport } from "@codemirror/lsp-client";
// Language
import { xml } from "@codemirror/lang-xml";
// no error checking
function simpleWebSocketTransport(uri) {
let handlers = [];
let sock = new WebSocket(uri);
sock.onmessage = e => { for (let h of handlers) h(e.data.toString()); };
return new Promise(resolve => {
sock.onopen = () => resolve({
send: (message) => sock.send(message),
subscribe: (handler) => handlers.push(handler),
unsubscribe: (handler) => handlers = handlers.filter(h => h != handler)
});
});
};
function lsp(transport,fileId){
let client = new LSPClient().connect(transport);
return languageServerSupport(client, fileId);
};
const baseExts = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
indentUnit.of(" "),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
indentWithTab,
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...searchKeymap,
]),
xml(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
];
function createEditorState(initialContents, extensions,options = {}) {
if (options.oneDark)
extensions.push(oneDark);
return EditorState.create({
doc: initialContents,
extensions
});
}
function createEditorView(state, parent) {
return new EditorView({ state, parent });
}
export { createEditorState, createEditorView, openSearchPanel, languageServerSupport, baseExts ,lsp, simpleWebSocketTransport};

View file

@ -3,14 +3,17 @@
: @author andy bunce
:)
module namespace lsprpc = 'lsprpc';
import module namespace lsp-text = 'lsp-text' at "lsp-text.xqm";
declare variable $lsprpc:methods:=map{
(: map methods to functions :)
declare variable $lsprpc:methods:=map:merge((
map{
"initialize" : lsprpc:method-initialize#1,
"initialized" : lsprpc:method-unknown#1,
"workspace/didChangeConfiguration" :lsprpc:method-unknown#1,
"textDocument/didOpen": lsprpc:method-unknown#1,
"textDocument/didClose" : lsprpc:method-unknown#1
};
"workspace/didChangeConfiguration" :lsprpc:method-unknown#1
},
$lsp-text:methods
));
(:~ return map if $msg is jsonrpc else empty :)
declare
@ -41,11 +44,28 @@ as empty-sequence() {
(:~ canned initialize response :)
declare
function lsprpc:method-initialize($json as map(*))
as map(*)?
as map(*)
{
``[{"jsonrpc":"2.0","id":0,
"result":{"capabilities":{"textDocumentSync":2,"completionProvider":{"resolveProvider":false,"triggerCharacters":["\"",":"]},"hoverProvider":true,"documentSymbolProvider":true,"documentRangeFormattingProvider":false,"colorProvider":{},"foldingRangeProvider":true,"selectionRangeProvider":true,"documentLinkProvider":{}}}}]``
=>parse-json()
{
"jsonrpc": "2.0",
"id": $json?id,
"result": {
"capabilities": {
"textDocumentSync": 2,
"completionProvider": {
"resolveProvider": false(),
"triggerCharacters": [ """", ":" ]
},
"hoverProvider": true(),
"documentSymbolProvider": true(),
"documentRangeFormattingProvider": false(),
"colorProvider": {},
"foldingRangeProvider": true(),
"selectionRangeProvider": true(),
"documentLinkProvider": {}
}
}
}
};
(:~ unknown method response :)

71
webapp/lsp/lsp-text.xqm Normal file
View file

@ -0,0 +1,71 @@
(:~ handle text messages
: @author andy bunce
:)
module namespace lsp-text = 'lsp-text';
import module namespace p="xq4" at "xq4.xqm";
declare variable $lsp-text:methods:=map{
"textDocument/didOpen": lsp-text:didOpen#1,
"textDocument/didClose" : lsp-text:method-unknown#1,
"textDocument/didChange": lsp-text:method-unknown#1,
"textDocument/hover": lsp-text:hover#1,
"textDocument/completion": lsp-text:completion#1
};
(:~ hover
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///session1.json"
},
"position": {
"line": 2,
"character": 22
}
}
}
:)
declare
function lsp-text:hover($json as map(*))
as map(*)?
{
let $doc:=$json?params?textDocument?uri
return map{
"jsonrpc": "2.0",
"id": $json?id,
"value":"uri: " || $doc
}
};
declare
function lsp-text:completion($json as map(*))
as map(*)?
{
let $doc:=$json?params?textDocument?uri
return map{
"jsonrpc": "2.0",
"id": $json?id,
"result":()
}
};
(:~ didOpen method response :)
declare
function lsp-text:didOpen($json as map(*))
as map(*)?
{
let $textDoc:=$json?params?textDocument
let $text:=$textDoc?text=>trace("TXT")
return ()
};
(:~ unknown method response :)
declare
function lsp-text:method-unknown($json as map(*))
as map(*)?
{
let $_:=trace($json?method,"unknown")
return ()
};

View file

@ -19,9 +19,13 @@ function lsp-ws:error($error) {
declare
%ws:connect('/lsp')
function lsp-ws:connect() as empty-sequence() {
ws:set(ws:id()=>trace("CONNECT: "), $chat-util:id, "session:get($chat-util:id)")
(: ,chat-util:users() :)
let $id:=ws:id()=>trace("CONNECT: ")
return (
ws:set($id, "id", $id),
store:clear(),
store:put("id",$id),
store:write("lsp-store")
)
};
(:~

6570
webapp/lsp/xq4.xqm Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,80 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codemirror6 example using BaseX LSP</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>BaseX LSP client <button id="search">🔍</button>
<label for="symbols">Symbols:</label><select id="symbols" disabled="disabled"></select>
<div style="float:right">
<input id="iServer" type="text" value="ws://localhost:3000/ws/lsp" /><button id="connect">connect</button>
</div>
</header>
<div class="container">
<div class="item">stuff</div>
<!-- Editor goes in here -->
<div id="editor" class="item"></div>
</div>
<!-- CodeMirror 6 -->
<script src="./lsp.bundle.js"></script>
<script>
const server = "ws://localhost:3000/ws/lsp";
let doc = `/**
*
* @param {string[]} items
* @param nada
*/
function foo(items, nada) {
for (var i=0; i<items.length; i++) {
alert(items[i] + 'juhu');
} // Real Tab.
//
//
}`;
// Load saved content from localStorage when the page loads
window.addEventListener('load', () => {
const savedText = localStorage.getItem('code');
if (savedText) {
doc = savedText;
}
});
const view = lsp.createEditorView(undefined, document.getElementById("editor"));
view.setState(lsp.createEditorState(doc, lsp.baseExts));
// Save content to localStorage when the page is about to unload
window.addEventListener('beforeunload', () => {
const doc = view.state.doc.toString();
localStorage.setItem('code', doc);
});
document.getElementById("connect").onclick = e => {
const v = document.getElementById("iServer").value;
alert(v)
};
document.getElementById("search").onclick = e => {
lsp.openSearchPanel(view);
};
lsp.simpleWebSocketTransport(server)
.then(transport => {
let link = lsp.lsp(transport, "file:///some/file.xml");
const state = lsp.createEditorState(doc, [...lsp.baseExts, link]);
view.setState(state);
})
.catch(r => alert("fail"));
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,27 @@
/* Set editor dimensions */
#editor {
height: 400px;
width: 50%;
}
/* Stretch editor to fit inside its containing div */
.cm-editor {
height: 100%;
width: 100%;
}
/* header */
header {
background-color: burlywood;
}
.container {
display: flex;
}
.item {
flex-grow: 1;
height: 100px;
}
.item + .item {
margin-left: 2%;
}