[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. 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 ## 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 ### Dev notes
State is held in websocket attributes. State is held in websocket attributes.
@ -13,12 +13,17 @@ State is held in websocket attributes.
|file-{uuid}|name of websocket attribute with textDocument| |file-{uuid}|name of websocket attribute with textDocument|
|parse-{uuid}|name of websocket attribute with parse tree XML| |parse-{uuid}|name of websocket attribute with parse tree XML|
## Sample html clients ## 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 * `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` ### 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
## 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/
I needed `set NODE_OPTIONS=--max_old_space_size=8192` for build to complete 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` r/bin/webpack-dev-server.js`
## Notes ## Related links
java -cp org.eclipse.lemminx-uber.jar org.eclipse.lemminx.XMLServerSocketLauncher` 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 Make a websocket server for lsp on port 3000
https://mkslanc.github.io/ace-linters/websocket.html 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://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 $uri:=$params?textDocument?uri
let $key:=docs:key($socket,$uri,"textDocument") let $key:=docs:key($socket,$uri,"textDocument")
let $file:=if(exists($key)) let $file:=if(exists($key))
then ws:get($socket,$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 $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) 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) let $file:=$file=>map:put("version",$ver)=>map:put("text",$text)
return ( ws:set($socket,$key,$file) return ( ws:set($socket,$key,$file)
@ -80,7 +84,7 @@ ws:get($socket,"files",{})=>map:keys()
}; };
(: document info :) (: document info :)
declare type docs:property as enum( declare type docs:Property as enum(
"textDocument", "textDocument",
"parse" "parse"
); );
@ -89,7 +93,7 @@ declare type docs:property as enum(
declare function docs:key( declare function docs:key(
$socket as xs:string, $socket as xs:string,
$file as xs:string, $file as xs:string,
$property as docs:property $property as docs:Property
) as xs:string? ) as xs:string?
{ {
ws:get($socket,"files")($file)($property) ws:get($socket,"files")($file)($property)
@ -99,7 +103,7 @@ declare function docs:key(
declare function docs:get( declare function docs:get(
$socket as xs:string, $socket as xs:string,
$file as xs:string, $file as xs:string,
$property as docs:property $property as docs:Property
) as item()? ) as item()?
{ {
ws:get($socket, ws:get($socket,
@ -117,6 +121,6 @@ declare function docs:parse(
let $xml:= p:parse-Module($text)=>prof:time("⏱️ p:parse-Module " || $file) let $xml:= p:parse-Module($text)=>prof:time("⏱️ p:parse-Module " || $file)
return ( return (
ws:set($socket,docs:key($socket,$file,"parse"),$xml), 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" }] "documentSelector": [{ "language": "xquery" }]
}, },
"hoverProvider": false, "hoverProvider": false,
"documentSymbolProvider": true, "documentSymbolProvider": false,
"documentRangeFormattingProvider": false, "documentRangeFormattingProvider": false,
"colorProvider": {}, "colorProvider": {},
"foldingRangeProvider": true, "foldingRangeProvider": false,
"selectionRangeProvider": true, "selectionRangeProvider": false,
"documentLinkProvider": {}, "documentLinkProvider": {},
"serverInfo": { "serverInfo": {
"name": "XQuery 4.0b Language Server", "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"; import module namespace lsp-text = 'lsp-text' at "lsp-text.xqm";
(: map methods to functions :) (: map methods to functions :)
declare variable $rpc:methods:=map:merge(( declare variable $rpc:Methods:=map:merge((
map{ map{
"initialize" : rpc:method-initialize#1, "initialize" : rpc:method-initialize#1,
"initialized" : rpc:method-initialized#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 declare
function rpc:reply($json as map(*)) function rpc:reply($json as map(*))
as empty-sequence() { as empty-sequence() {
let $method := $json?method let $f :=$rpc:Methods?($json?method)
let $f :=$rpc:methods?($method)
let $response := $f!.($json) 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 :) (:~ canned initialize response :)

View file

@ -2,30 +2,17 @@ module namespace lint="lsp/lint";
(: (:
Describes a problem or hint for a piece of code. 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. markClass?: string When given, add an extra CSS class to parts of the code that this diagnostic applies to.
to: number
The end position. May be equal to from, though actually covering text is preferable. source?: string An optional source string indicating where the diagnostic is coming from. You can put the name of your linter here, if applicable.
severity: "error" | "hint" | "info" | "warning"
The severity of the problem. This will influence how it is displayed. message: string The message associated with this diagnostic.
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.
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'; module namespace lsp-diags = 'lsp-diags';
import module namespace docs="lsp/docs" at "docs.xqm";
import module namespace pos="lsp/position" at "position.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( declare function lsp-diags:publish(
$uri as xs:string, $uri as xs:string,
$xml as element(Module|ERROR)) $text as xs:string,
$xml as lsp-diags:ParseResult)
as map(*){ as map(*){
let $diagnostics:=[] let $diagnostics:=if($xml/self::ERROR)
=>array:append(pos:nostic(pos:Range(pos:Position(0,1),pos:Position(0,3)),1,"A test")) then [lsp-diags:parse-error($text, $xml)]
else []
return {"jsonrpc": "2.0", return {"jsonrpc": "2.0",
"method":"textDocument/publishDiagnostics", "method":"textDocument/publishDiagnostics",
"params":{"uri": $uri, "diagnostics": $diagnostics} "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(*)) declare function pos:resolvePosition($text as xs:string, $pos as map(*))
as xs:integer 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( let $s:= while-do(
map{"off":0,"line":0}, map{"off":0,"line":0},
fn($r, $i){$r?line < $pos?line}, fn($r, $i){$r?line < $pos?line},
fn($r,$i){ let $next:=$nl[$i] fn($r,$i){ let $next:=$nl[$i]
return if(empty($next)) return if(empty($next))
then error(xs:QName("docs:range"),"bad line") 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 let $off:= $s?off+$pos?character
return if($off>string-length($text)) return if($off>string-length($text))
@ -59,9 +60,11 @@ as xs:integer
(:~ convert index into Position :) (:~ convert index into Position :)
declare function pos:toPosition($text as xs:string, $index as xs:integer) declare function pos:toPosition($text as xs:string, $index as xs:integer)
as pos:Position { 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 $predicate:=fn($r, $i){
let $next:=$nl[$i=>trace("I ")] let $next:=$nl[$i]
return exists($next) and $next < $index return exists($next) and $next < $index
} }
@ -73,19 +76,4 @@ as pos:Position {
return pos:Position($s?line, $index - $s?off) 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 => { document.getElementById("search").onclick = e => {
lsp.openSearchPanel(view); lsp.openSearchPanel(view);
}; };
document.getElementById("lint").onclick = e => { document.getElementById("lint").onclick = async e => {
console.log("word",view.state.wordAt(1)); 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); lsp.openLintPanel(view);
}; };
document.getElementById("load").onchange = e => { document.getElementById("load").onchange = e => {