[fix] position

This commit is contained in:
Andy Bunce 2025-08-10 23:18:01 +01:00
parent e3766ee6a4
commit 8f2368ce7b
9 changed files with 88 additions and 67 deletions

View file

@ -2,7 +2,7 @@ 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.
## 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 message 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.
@ -13,12 +13,17 @@ State is held in websocket attributes.
|file-{uuid}|name of websocket attribute with textDocument|
|parse-{uuid}|name of websocket attribute with parse tree XML|
## Sample html clients
### Using `CodeMirror6`
### 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
#### uses
* https://github.com/FurqanSoftware/codemirror-languageserver
* https://hjr265.me/blog/codemirror-lsp/
### 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
## Uses
#### Uses
* https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/
I needed `set NODE_OPTIONS=--max_old_space_size=8192` for build to complete
@ -27,9 +32,9 @@ Or `node --max-old-space-size=8192 node_modules/webpack-dev-serve
r/bin/webpack-dev-server.js`
## Notes
## Related links
java -cp org.eclipse.lemminx-uber.jar org.eclipse.lemminx.XMLServerSocketLauncher`
Using https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/
Make a websocket server for lsp on port 3000
https://mkslanc.github.io/ace-linters/websocket.html
@ -38,5 +43,5 @@ http://localhost:3000/exampleServer
https://github.com/mkslanc/ace-linters/blob/c1b317e01299016ac7da6588361228637f4eac25/packages/demo/websockets-lsp/server/server.ts
https://www.jsonrpc.org/specification

View file

@ -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"}]}
{"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\")?text\r\nreturn string-to-codepoints($t)=>index-of(10)"}]}

View file

@ -45,13 +45,17 @@ declare function docs:change(
{
let $uri:=$params?textDocument?uri
let $key:=docs:key($socket,$uri,"textDocument")
let $file:=if(exists($key))
then ws:get($socket,$key)
else error("no file")
else error(xs:QName("docs:change"), "no file" || $uri)
let $ver:=$params?textDocument?version
let $text:=if($ver eq 1+ $file?version)
let $IGNORE_VER:=true()
let $text:=if($IGNORE_VER or $ver eq 1+ $file?version)
then pos:apply-changes($file?text,$params?contentChanges)
else error("bad ver")
else error(xs:QName("docs:change"),`badver got {$ver} expecting: {1+ $file?version}`)
let $file:=$file=>map:put("version",$ver)=>map:put("text",$text)
return ( ws:set($socket,$key,$file)
@ -80,7 +84,7 @@ ws:get($socket,"files",{})=>map:keys()
};
(: document info :)
declare type docs:property as enum(
declare type docs:Property as enum(
"textDocument",
"parse"
);
@ -89,7 +93,7 @@ declare type docs:property as enum(
declare function docs:key(
$socket as xs:string,
$file as xs:string,
$property as docs:property
$property as docs:Property
) as xs:string?
{
ws:get($socket,"files")($file)($property)
@ -99,7 +103,7 @@ declare function docs:key(
declare function docs:get(
$socket as xs:string,
$file as xs:string,
$property as docs:property
$property as docs:Property
) as item()?
{
ws:get($socket,
@ -117,6 +121,6 @@ declare function docs:parse(
let $xml:= p:parse-Module($text)=>prof:time("⏱️ p:parse-Module " || $file)
return (
ws:set($socket,docs:key($socket,$file,"parse"),$xml),
lsp-diags:publish($file,$xml)
lsp-diags:publish($file, $text, $xml)
)
};

View file

@ -7,11 +7,11 @@
"documentSelector": [{ "language": "xquery" }]
},
"hoverProvider": false,
"documentSymbolProvider": true,
"documentSymbolProvider": false,
"documentRangeFormattingProvider": false,
"colorProvider": {},
"foldingRangeProvider": true,
"selectionRangeProvider": true,
"foldingRangeProvider": false,
"selectionRangeProvider": false,
"documentLinkProvider": {},
"serverInfo": {
"name": "XQuery 4.0b Language Server",

View file

@ -6,7 +6,7 @@ module namespace rpc = 'rpc';
import module namespace lsp-text = 'lsp-text' at "lsp-text.xqm";
(: map methods to functions :)
declare variable $rpc:methods:=map:merge((
declare variable $rpc:Methods:=map:merge((
map{
"initialize" : rpc:method-initialize#1,
"initialized" : rpc:method-initialized#1,
@ -31,14 +31,17 @@ as map(*)?
};
(:~ send replay to $json :)
(:~ send reply to $json message
get functions for methods
evaluate function with message
send any responses
:)
declare
function rpc:reply($json as map(*))
as empty-sequence() {
let $method := $json?method
let $f :=$rpc:methods?($method)
let $f :=$rpc:Methods?($json?method)
let $response := $f!.($json)
return if(exists($response)) then ws:send($response=>trace("REPLY: "),ws:id())
return $response!ws:send(.=>trace("REPLY: "),ws:id())
};
(:~ canned initialize response :)

View file

@ -2,30 +2,17 @@ module namespace lint="lsp/lint";
(:
Describes a problem or hint for a piece of code.
from: number
from: number The start position of the relevant text.
to: number The end position. May be equal to from, though actually covering text is preferable.
severity: "error" | "hint" | "info" | "warning". The severity of the problem. This will influence how it is displayed.
The start position of the relevant text.
to: number
markClass?: string When given, add an extra CSS class to parts of the code that this diagnostic applies to.
The end position. May be equal to from, though actually covering text is preferable.
severity: "error" | "hint" | "info" | "warning"
source?: string An optional source string indicating where the diagnostic is coming from. You can put the name of your linter here, if applicable.
The severity of the problem. This will influence how it is displayed.
markClass?: string
When given, add an extra CSS class to parts of the code that this diagnostic applies to.
source?: string
An optional source string indicating where the diagnostic is coming from. You can put the name of your linter here, if applicable.
message: string
The message associated with this diagnostic.
renderMessage?: fn(view: EditorView) → Node
An optional custom rendering function that displays the message as a DOM node.
actions?: readonly Action[]
An optional array of actions that can be taken on this diagnostic.
message: string The message associated with this diagnostic.
renderMessage?: fn(view: EditorView) → Node An optional custom rendering function that displays the message as a DOM node.
actions?: readonly Action[] An optional array of actions that can be taken on this diagnostic.
:)

View file

@ -1,17 +1,46 @@
module namespace lsp-diags = 'lsp-diags';
import module namespace docs="lsp/docs" at "docs.xqm";
import module namespace pos="lsp/position" at "position.xqm";
declare type lsp-diags:ParseResult as element(Module|ERROR);
(:~
from: number The start position of the relevant text.
to: number The end position. May be equal to from, though actually covering text is preferable.
severity: "error" | "hint" | "info" | "warning" The severity of the problem. This will influence how it is displayed.
markClass?: string When given, add an extra CSS class to parts of the code that this diagnostic applies to.
source?: string An optional source string indicating where the diagnostic is coming from. You can put the name of your linter here, if applicable.
message: string The message associated with this diagnostic.
renderMessage?: fn(view: EditorView) → Node An optional custom rendering function that displays the message as a DOM node.
actions?: readonly Action[] An optional array of actions that can be taken on this diagnostic.
:)
declare record lsp-diags:nostic(
range as pos:Range,
severity as xs:integer, (: enum('error', 'hint', 'info', 'warning') :)
message as xs:string
);
declare function lsp-diags:publish(
$uri as xs:string,
$xml as element(Module|ERROR))
$text as xs:string,
$xml as lsp-diags:ParseResult)
as map(*){
let $diagnostics:=[]
=>array:append(pos:nostic(pos:Range(pos:Position(0,1),pos:Position(0,3)),1,"A test"))
let $diagnostics:=if($xml/self::ERROR)
then [lsp-diags:parse-error($text, $xml)]
else []
return {"jsonrpc": "2.0",
"method":"textDocument/publishDiagnostics",
"params":{"uri": $uri, "diagnostics": $diagnostics}
}
};
(:~
<ERROR b="10819" e="10820" o="234" s="43">syntax error, found '}' while expecting [S,'else'] at line 290, column 3: ...} }; ? return bookmark info for children of $outlineItem as s...</ERROR>
:)
declare function lsp-diags:parse-error($text as xs:string, $xml as element(ERROR))
as map(*)?{
lsp-diags:nostic(pos:Range(pos:toPosition($xml, $xml/@b),
pos:toPosition($xml, $xml/@e)),
1,$xml/string())
};

View file

@ -40,14 +40,15 @@ as xs:string{
declare function pos:resolvePosition($text as xs:string, $pos as map(*))
as xs:integer
{
let $nl:=characters($text)=>index-of(char("\n"))
let $nl:= index-of(string-to-codepoints($text),10)
let $_:=trace(count($nl),"lines ")
let $s:= while-do(
map{"off":0,"line":0},
fn($r, $i){$r?line < $pos?line},
fn($r,$i){ let $next:=$nl[$i]
return if(empty($next))
then error(xs:QName("docs:range"),"bad line")
else {"off": $next+1,"line":$r?line+1}=>trace("AA ") }
else {"off": $next+1,"line":$r?line+1} }
)
let $off:= $s?off+$pos?character
return if($off>string-length($text))
@ -59,9 +60,11 @@ as xs:integer
(:~ convert index into Position :)
declare function pos:toPosition($text as xs:string, $index as xs:integer)
as pos:Position {
let $nl:=characters($text)=>index-of(char("\n"))
let $nl:= index-of(string-to-codepoints($text),10)
let $_:=trace(count($nl),"lines ")
let $predicate:=fn($r, $i){
let $next:=$nl[$i=>trace("I ")]
let $next:=$nl[$i]
return exists($next) and $next < $index
}
@ -73,19 +76,4 @@ as pos:Position {
return pos:Position($s?line, $index - $s?off)
};
(:~
from: number The start position of the relevant text.
to: number The end position. May be equal to from, though actually covering text is preferable.
severity: "error" | "hint" | "info" | "warning" The severity of the problem. This will influence how it is displayed.
markClass?: string When given, add an extra CSS class to parts of the code that this diagnostic applies to.
source?: string An optional source string indicating where the diagnostic is coming from. You can put the name of your linter here, if applicable.
message: string The message associated with this diagnostic.
renderMessage?: fn(view: EditorView) → Node An optional custom rendering function that displays the message as a DOM node.
actions?: readonly Action[] An optional array of actions that can be taken on this diagnostic.
:)
declare record pos:nostic(
range as pos:Range,
severity as xs:integer, (: enum('error', 'hint', 'info', 'warning') :)
message as xs:string
);

View file

@ -27,8 +27,13 @@ document.getElementById("connect").onclick = e => {
document.getElementById("search").onclick = e => {
lsp.openSearchPanel(view);
};
document.getElementById("lint").onclick = e => {
document.getElementById("lint").onclick = async e => {
console.log("word",view.state.wordAt(1));
const ser=document.getElementById("iServer").value;
//const transport = new WebTransport(ser);
// The connection can be used once ready fulfills
//await transport.ready;
lsp.openLintPanel(view);
};
document.getElementById("load").onchange = e => {