diff --git a/README.md b/README.md index 4df3db5..a008a9c 100644 --- a/README.md +++ b/README.md @@ -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 + diff --git a/docs/explore.xqbk b/docs/explore.xqbk index 861444c..9854d1c 100644 --- a/docs/explore.xqbk +++ b/docs/explore.xqbk @@ -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"}]} \ No newline at end of file +{"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)"}]} \ No newline at end of file diff --git a/webapp/lsp/docs.xqm b/webapp/lsp/docs.xqm index 9f46a20..d60c076 100644 --- a/webapp/lsp/docs.xqm +++ b/webapp/lsp/docs.xqm @@ -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) ) }; \ No newline at end of file diff --git a/webapp/lsp/etc/capabilities.json b/webapp/lsp/etc/capabilities.json index 007d2cb..055451b 100644 --- a/webapp/lsp/etc/capabilities.json +++ b/webapp/lsp/etc/capabilities.json @@ -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", diff --git a/webapp/lsp/jsonrpc.xqm b/webapp/lsp/jsonrpc.xqm index 352dca2..11f41a2 100644 --- a/webapp/lsp/jsonrpc.xqm +++ b/webapp/lsp/jsonrpc.xqm @@ -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 :) diff --git a/webapp/lsp/lint.xqm b/webapp/lsp/lint.xqm index 8e39c19..843a98e 100644 --- a/webapp/lsp/lint.xqm +++ b/webapp/lsp/lint.xqm @@ -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. :) \ No newline at end of file diff --git a/webapp/lsp/lsp-diags.xqm b/webapp/lsp/lsp-diags.xqm index c542de5..42d467e 100644 --- a/webapp/lsp/lsp-diags.xqm +++ b/webapp/lsp/lsp-diags.xqm @@ -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} } }; + +(:~ +syntax error, found '}' while expecting [S,'else'] at line 290, column 3: ...} }; ? return bookmark info for children of $outlineItem as s... +:) +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()) +}; \ No newline at end of file diff --git a/webapp/lsp/position.xqm b/webapp/lsp/position.xqm index a10a8ef..db11b01 100644 --- a/webapp/lsp/position.xqm +++ b/webapp/lsp/position.xqm @@ -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 -); diff --git a/webapp/static/clients/codemirror/script.js b/webapp/static/clients/codemirror/script.js index 4f1644c..82a8fc3 100644 --- a/webapp/static/clients/codemirror/script.js +++ b/webapp/static/clients/codemirror/script.js @@ -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 => {