[mod] local bootstrap

This commit is contained in:
Andy Bunce 2025-08-04 12:34:23 +01:00
parent ce69c61b6c
commit 74f2c74fb4
14 changed files with 387 additions and 33 deletions

View file

@ -1,6 +1,6 @@
Work in progress.
An attempt to write a [Language Server Protocol](https://en.wikipedia.org/wiki/Language_Server_Protocol) server using 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 using XQuery 4.0 and the [BaseX websocket](https://docs.basex.org/main/WebSockets) feature.
## 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 format.
## Sample clients
@ -26,4 +26,3 @@ https://github.com/mkslanc/ace-linters/blob/c1b317e01299016ac7da6588361228637f4e
https://www.jsonrpc.org/specification
yyy **GGG**

View file

@ -0,0 +1,15 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///some/file.xml",
"version": 1
},
"contentChanges": [
{
"text": "3+1f"
}
]
}
}

View file

@ -14,8 +14,8 @@
"selectionRangeProvider": true,
"documentLinkProvider": {},
"serverInfo": {
"name": "My Custom Language Server",
"version": "1.0.0"
"name": "BaseX Language Server",
"version": "0.0.1"
}
}
}

View file

@ -6,8 +6,19 @@
module namespace docs="lsp/docs";
import module namespace p="xq4" at "xq4.xqm";
declare record docs:Position(
(: Line position in a document (zero-based). :)
line as xs:integer,
(: Character offset on a line in a document (zero-based). :)
character as xs:integer
);
(: document info :)
declare type docs:property as enum("textDocument","parse");
declare type docs:property as enum(
"textDocument",
"parse"
);
(: get $property for $file from session $socket :)
declare function docs:get(
@ -43,3 +54,28 @@ return (
ws:set($socket,$keys?parse,$xml)
)
};
declare function docs:resolvePosition(text as xs:string, pos as docs:Position)
as xs:integer
{
let line = 0, off = 0
while (line < pos.line) {
let next = text.indexOf("\n", off)
if (!next) throw new RangeError("Position out of bounds")
off = next + 1
line++
}
off += pos.character
if (off > string-length($text)) throw new RangeError("Position out of bounds")
return off
}
declare function docs:toPosition($text as xs:string, $pos as xs:integer)
as docs:Position {
for (let off = 0, line = 0;;) {
let next = text.indexOf("\n", off)
if (next < 0 || next >= pos) return {line, character: pos - off}
off = next + 1
line++
}
}

View file

@ -2,15 +2,17 @@
: @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/didChange": lsp-text:didChange#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
:)
@ -53,18 +55,32 @@ as map(*)?
(:~ didOpen method response :)
declare
function lsp-text:didOpen($json as map(*))
as map(*)?
as empty-sequence()
{
let $textDoc:=$json?params?textDocument
let $x:=job:eval(xs:anyURI("parse.xq"),
{"textDocument":$textDoc,"webSocket":ws:id()},
{ 'cache': true() }
)
return ws:set(ws:id(),$textDoc?uri,$textDoc)
return ()
};
(:~ didOpen method response :)
declare
function lsp-text:didChange($json as map(*))
as map(*)?
{
let $textDoc:=$json?params?textDocument
let $x:=job:eval(xs:anyURI("parse.xq"),
{"textDocument":$textDoc,"webSocket":ws:id()},
{ 'cache': true() }
)
return ()
};
(:~ unknown method response :)
declare
function lsp-text:method-unknown($json as map(*))

279
webapp/lsp/snippets.jsonc Normal file
View file

@ -0,0 +1,279 @@
{
"new library module": {
"isFileTemplate": true,
"prefix": "library module",
"body": [
"xquery version '3.1';",
"(:~",
"@author: ",
"@date: $CURRENT_YEAR/$CURRENT_MONTH/$CURRENT_DATE",
":)",
"module namespace ${1:prefix} = '${2:http://www.example.com/}';",
""
],
"description": "New library module template"
},
"new main module": {
"isFileTemplate": true,
"prefix": "main module",
"body": [
"xquery version '3.1';",
"(:~",
":)",
"${1:expr}",
""
],
"description": "New main module template"
},
"flowr": {
"prefix": [
"for",
"flowr"
],
"body": [
"for \\$${1:var} at \\$${2:pos} in ${3:expr}",
"let \\$${4:var2} := ${5:expr}",
"where ${6:boolean}",
"order by ${7:expr}",
"return ${8:expr2}"
],
"description": "Full FLOWR expression"
},
"return": {
"prefix": "return",
"body": "return ${1:expr}"
},
"import": {
"prefix": "import",
"body": "import module namespace ${1:ns} = '${2:http://www.example.com/}';",
"description": "Import module"
},
"if": {
"prefix": "if",
"body": [
"if (${1:boolean})",
"then ${2:expr1}",
"else ${3:expr2}"
],
"description": "If then else expression"
},
"module": {
"prefix": "module",
"body": "module namespace ${1:ns} = '${2:http://www.example.com}';"
},
"every": {
"prefix": "every",
"body": "every \\$${1:varname} in ${2:expr} satisfies ${3:expr}"
},
"some": {
"prefix": "some",
"body": "some \\$${1:varname} in ${2:expr} satisfies ${3:expr}"
},
"declare namespace": {
"prefix": [
"declare",
"namespace"
],
"body": [
"declare ${1:prefix}='${2:namespace}';",
""
],
"description": "declare namespace"
},
"declare base-uri": {
"prefix": [
"declare",
"baseuri"
],
"body": [
"declare base-uri '${1:uriliteral}';",
""
],
"description": "declare base-uri"
},
"declare option": {
"prefix": [
"declare",
"option"
],
"body": [
"declare option ${1:eqname} '${2:string}';",
""
],
"description": "declare option"
},
"declare function": {
"prefix": [
"declare",
"function"
],
"body": [
"(:~ ${1:name} :)",
"declare function ${2:ns}:${1:name}()",
"as ${3:type}{",
"${3:expr}",
"};",
""
],
"description": "declare function"
},
"declare variable": {
"prefix": [
"declare",
"variable"
],
"body": [
"(:~ \\$${1:varname} :)",
"declare variable \\$${1:varname} := ${2:expr};",
""
],
"description": "declare variable"
},
"switch": {
"prefix": "switch",
"body": [
"switch(${1:foo})",
"case ${2:foo} return ${3:true}",
"default return ${4:false}"
],
"description": "switch statement"
},
"typeswitch": {
"prefix": "type",
"body": [
"typeswitch(${1:foo})",
"case ${2:foo} return ${3:true}",
"default return ${4:false}"
],
"description": "typeswitch statement"
},
"try": {
"prefix": "try",
"body": [
"try {",
" ${1:expr}",
"} catch ${2:*}",
" { ${3:expr}",
"}"
],
"description": "try catch"
},
"tumbling": {
"prefix": [
"for",
"tumbling",
"window"
],
"body": [
"for tumbling window \\$${1:varname} in ${2:expr}",
"start at \\$${3:start} when ${4:expr}",
"end at \\$${5:end} when ${6:expr}",
"return ${7:expr}"
],
"description": "tumbling window"
},
"sliding": {
"prefix": [
"for",
"sliding",
"window"
],
"body": [
"for sliding window \\$${1:varname} in ${2:expr}",
"start at \\$${3:start} when ${4:expr}",
"end at \\$${5:end} when ${6:expr}",
"return ${7:expr}"
],
"description": "sliding window"
},
"let": {
"prefix": "let",
"body": "let \\$${1:varname} := ${2:expr}"
},
"castable": {
"body": "castable as ${1:atomicType}"
},
"cast": {
"body": "cast as ${1:atomicType}"
},
// Updates ***************
"update insert": {
"prefix": [
"update",
"insert"
],
"body": "insert node ${1:expr} into ${2:xpath}"
},
"update delete": {
"prefix": ["delete","update"],
"body": "delete node ${1:xpath}"
},
"update replace node": {
"prefix":["update","replace"],
"body": "replace node ${1:xpath} with ${2:expr}"
},
"update replace value": {
"prefix": [ "update",
"replace",
"value"
],
"body": "replace value of node ${1:xpath} with ${2:expr}"
},
"update rename": {
"prefix": [
"update",
"rename"
],
"body": "rename node ${1:xpath} as ${2:eqname}"
},
"copy modify return": {
"prefix": [
"copy",
"modify",
"return"
],
"body": [
"copy \\$${1:varname} := ${2:node}",
"modify ${3:updates}",
"return \\$${1:varname}"
]
},
"transform with": {
"prefix": [
"transform",
"with",
"update"
],
"body": [
"${1:node} transform with {",
" ${2:update}",
"}"
]
},
"transform update": {
"prefix": [
"transform",
"update"
],
"body": [
"${1:node} update {",
"${2:update}",
"}"
]
}
}
//snippet group
// group by $${1:varname} := ${2:expr}
//snippet order
// order by ${1:expr} ${2:descending}
//snippet stable
// stable order by ${1:expr}
//snippet count
// count $${1:varname}
//snippet ordered
// ordered { ${1:expr} }
//snippet unordered
// unordered { ${1:expr} }
//snippet treat
// treat as ${1:expr}

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
<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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"
<link href="bootstrap@5.3.7.css" rel="stylesheet"
integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
<link rel="stylesheet" href="styles.css" />
@ -15,28 +15,31 @@
<body>
<nav class="navbar bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand">BaseX LSP client</a>
<a href="/dba/logs" target="dba">#</a>
<a class="navbar-brand">BaseX LSP client</a>
<a href="/dba/logs" target="dba">#</a>
<form class="d-flex">
<span id="state">🔴</span>
<input id="iServer" type="text" value="ws://localhost:3000/ws/lsp" style="width:25em" />
<button id="connect">connect</button>
</form>
</div>
<form class="d-flex">
<span id="state">🔴</span>
<input id="iServer" type="text" value="ws://localhost:3000/ws/lsp" style="width:25em" />
<button id="connect">connect</button>
</form>
</div>
</nav>
<div class="container-fluid" >
<div class="row " >
<div class="container-fluid">
<div class="row ">
<div class="col-2">
something
</div>
<div class="col flex-grow-1" >
<div class="row" >
<div >
<div class="col flex-grow-1">
<div class="row">
<div>
<select id="language">
<option selected>Language</option>
<option value="plaintext">plaintext</option>
<option value="xquery">xquery</option>
<option value="xml">xml</option>
</select>
<label for="file">File:</label><input id="iFile" type="url" value="file:///some/file.xml" />
<button id="search">🔍</button>
<button id="lint">⚠️</button>
@ -47,7 +50,7 @@
<!-- Editor goes in here -->
<div id="editor"></div>
<div id="editor"></div>
</div>
</div>
@ -90,7 +93,7 @@
lsp.simpleWebSocketTransport(server)
.then(transport => {
let link = lsp.lsp(transport, file);
const doc=view.state.doc.toString();
const doc = view.state.doc.toString();
const state = lsp.createEditorState(doc, [...lsp.baseExts, link]);
view.setState(state);
})