From 1c758567d46f0084c4768120d3dd8b030111d53b Mon Sep 17 00:00:00 2001 From: Andy Bunce Date: Fri, 11 Jul 2025 15:16:28 +0100 Subject: [PATCH] [mod] merge lsp-web-poc --- .dockerignore | 8 + README.md | 11 +- compose.yaml | 20 + protocol.md | 25 + setup.bxs | 2 + src/server/html/acego.js | 19 + src/server/html/index.html | 34 ++ src/server/html/t2.html | 39 ++ src/server/lsp.xqm | 72 +++ webapp/lsp/lsp-util.xqm | 56 +++ webapp/lsp/lsp-ws.xqm | 46 ++ webapp/lsp/lsp.xqm | 144 ++++++ websockets-lsp/client.ts | 30 ++ websockets-lsp/server/.gitignore | 1 + websockets-lsp/server/README.md | 11 + websockets-lsp/server/package-lock.json | 617 ++++++++++++++++++++++++ websockets-lsp/server/package.json | 12 + websockets-lsp/server/server.ts | 98 ++++ 18 files changed, 1243 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 compose.yaml create mode 100644 protocol.md create mode 100644 setup.bxs create mode 100644 src/server/html/acego.js create mode 100644 src/server/html/index.html create mode 100644 src/server/html/t2.html create mode 100644 src/server/lsp.xqm create mode 100644 webapp/lsp/lsp-util.xqm create mode 100644 webapp/lsp/lsp-ws.xqm create mode 100644 webapp/lsp/lsp.xqm create mode 100644 websockets-lsp/client.ts create mode 100644 websockets-lsp/server/.gitignore create mode 100644 websockets-lsp/server/README.md create mode 100644 websockets-lsp/server/package-lock.json create mode 100644 websockets-lsp/server/package.json create mode 100644 websockets-lsp/server/server.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..704cfa7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ + +#Ignoring git and other folders +.git +.github +.settings +src/ +#Ignoring all the markdown +*.md \ No newline at end of file diff --git a/README.md b/README.md index 01f40ea..7c4a044 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ -# basex-lsp +Using https://github.com/mkslanc/ace-linters https://mkslanc.github.io/ace-linters/ -language server protocol \ No newline at end of file +Make a websocket server for lsp on port 3000 +https://mkslanc.github.io/ace-linters/websocket.html + +http://localhost:3000/exampleServer + +https://github.com/mkslanc/ace-linters/blob/c1b317e01299016ac7da6588361228637f4eac25/packages/demo/websockets-lsp/server/server.ts + +https://www.jsonrpc.org/specification \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..e3f6b99 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,20 @@ +# basex lsp, use with +services: + basex: + image: ghcr.io/quodatum/basexhttp:basex-12.0 + container_name: basex-lsp + restart: unless-stopped + ports: + - "3000:8080" + - "3001:1984" + volumes: + - basex-lsp:/srv/basex/data + - ./webapp/lsp:/srv/basex/webapp/lsp + - ./setup.bxs:/srv/basex/setup.bxs +# - ./jars:/srv/basex/lib/custom +# - ./repo:/srv/basex/repo + environment: + - "SERVER_OPTS= -c basex/setup.bxs" + +volumes: + basex-lsp: diff --git a/protocol.md b/protocol.md new file mode 100644 index 0000000..9706b10 --- /dev/null +++ b/protocol.md @@ -0,0 +1,25 @@ +lsp websocket messages + +```json +{"jsonrpc":"2.0","id":0, +"method":"initialize", +"params":{"capabilities":{"textDocument":{"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"synchronization":{"dynamicRegistration":true,"willSave":false,"didSave":false,"willSaveWaitUntil":false},"formatting":{"dynamicRegistration":true},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":false,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":false,"preselectSupport":false},"contextSupport":false},"signatureHelp":{"signatureInformation":{"documentationFormat":["markdown","plaintext"],"activeParameterSupport":true}},"documentHighlight":{"dynamicRegistration":true},"semanticTokens":{"multilineTokenSupport":false,"overlappingTokenSupport":false,"tokenTypes":[],"tokenModifiers":[],"formats":["relative"],"requests":{"full":{"delta":false},"range":true},"augmentsSyntaxTokens":true}},"workspace":{"didChangeConfiguration":{"dynamicRegistration":true}}},"processId":null,"rootUri":"","workspaceFolders":null} +} +``` +<= +{"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":{}}} +} + +{"jsonrpc":"2.0","method":"initialized","params":{}} + +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{}}} + + +<= +{"jsonrpc":"2.0", +"method":"textDocument/publishDiagnostics", +"params":{"uri":"session1.json","diagnostics":[]} +} +<= +{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"session1.json","diagnostics":[{"range":{"start":{"line":2,"character":7},"end":{"line":2,"character":16}},"message":"Expected comma","severity":1,"code":514,"source":"json"}]}} \ No newline at end of file diff --git a/setup.bxs b/setup.bxs new file mode 100644 index 0000000..403be29 --- /dev/null +++ b/setup.bxs @@ -0,0 +1,2 @@ +CREATE DB xmark http://files.basex.org/xml/xmark.xml +REPO INSTALL https://github.com/expkg-zone58/pdfbox/releases/download/v0.3.6/pdfbox-0.3.6.xar \ No newline at end of file diff --git a/src/server/html/acego.js b/src/server/html/acego.js new file mode 100644 index 0000000..8138df3 --- /dev/null +++ b/src/server/html/acego.js @@ -0,0 +1,19 @@ +import * as ace from "ace-builds/ace"; +import {Mode as JSONMode} from "ace-builds/src/mode/json"; // any mode you want +import {AceLanguageClient} from "ace-linters/build/ace-language-client"; + +const serverData = { + module: () => import("ace-linters/build/language-client"), + modes: "json|json5", + type: "socket", + socket: new WebSocket("ws://127.0.0.1:3000/ws/lsp"), // your websocket server address +} + +// Create an Ace editor +let editor = ace.edit("container", { + mode: new JSONMode() +}); + +// Create a language provider for WebSocket +let languageProvider = AceLanguageClient.for(serverData); +languageProvider.registerEditor(editor); \ No newline at end of file diff --git a/src/server/html/index.html b/src/server/html/index.html new file mode 100644 index 0000000..6264a29 --- /dev/null +++ b/src/server/html/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + +
some text
+ + + + + \ No newline at end of file diff --git a/src/server/html/t2.html b/src/server/html/t2.html new file mode 100644 index 0000000..4f19cec --- /dev/null +++ b/src/server/html/t2.html @@ -0,0 +1,39 @@ + + + + + + Editor + + + + +
function foo(items) {
+    var i;
+    for (i = 0; i < items.length; i++) {
+        alert("Ace Rocks " + items[i]);
+    }
+}
+ + + + + + \ No newline at end of file diff --git a/src/server/lsp.xqm b/src/server/lsp.xqm new file mode 100644 index 0000000..98916bf --- /dev/null +++ b/src/server/lsp.xqm @@ -0,0 +1,72 @@ +(:~ web socket for ace +:) +module namespace lsp="urn:quodatum:vscode"; +import module namespace ws = "http://basex.org/modules/ws"; + +declare +%ws:connect('/ws/exampleServer') +function lsp:connect() +as empty-sequence(){ + let $_:=trace(ws:id(),"BaseX socket id: ") + return () + }; + + (:~ + : Processes a WebSocket message. + : @param $message message + :) +declare + %ws:message('/ws/exampleServer', '{$message}') +function lsp:message( + $message as xs:string +) as empty-sequence() { + let $json:=json:parse($message, map{ 'format': 'xquery' }) + let $id:=$json?id + let $_:=trace($id,"MESSAGE id: ") + return switch($json?method=>trace("method: ")) + case "initialize" return + let $a:=map{ + "capabilities":map{"textDocumentSync":2, + "completionProvider":map{"resolveProvider":false(),"triggerCharacters":["\",":"]}, + "hoverProvider":true(), + "documentSymbolProvider":true(), + "documentRangeFormattingProvider":false(), + "colorProvider":map{}, + "foldingRangeProvider":true(), + "selectionRangeProvider":true(), + "documentLinkProvider":map{} + } + } + let $a:=trace($a,"A: ") + return lsp:send-result($a,$id,ws:id()) + default + return error() +}; + +declare %ws:error('/ws/exampleServer', '{$error}') +function lsp:error($error) +{ + let $_:=trace($error,"ERROR ") + return () +}; + +(:~ + : Closes a WebSocket connection. Unregisters the user and notifies all clients. + :) +declare + %ws:close('/ws/exampleServer') +function lsp:close() as empty-sequence() { + let $_:=trace("CLOSE") + return () +}; + + + +declare +function lsp:send-result($result as map(*),$rpcId as xs:string, $wsId as xs:string) +as empty-sequence() +{ + let $json:=map{"jsonrpc":"2.0","id":$rpcId,"result":$result}=>trace("RESULT: ") + return ws:send($json,$wsId) +}; + diff --git a/webapp/lsp/lsp-util.xqm b/webapp/lsp/lsp-util.xqm new file mode 100644 index 0000000..20396e5 --- /dev/null +++ b/webapp/lsp/lsp-util.xqm @@ -0,0 +1,56 @@ +(:~ + : Simple WebSocket chat. Utility functions. + : @author BaseX Team, BSD License + :) +module namespace chat-util = 'chat/util'; + +import module namespace session = 'http://basex.org/modules/session'; +import module namespace ws = 'http://basex.org/modules/ws'; + +(:~ User id (bound to sessions and WebSockets). :) +declare variable $chat-util:id := 'id'; + +(:~ + : Sends a users list (all, active) to all registered clients. + :) +declare function chat-util:users() as empty-sequence() { + ws:emit({ + 'type': 'users', + 'users': array { sort(user:list()) }, + 'active': array { distinct-values( + for $id in ws:ids() + return ws:get($id, $chat-util:id) + )} + }) +}; + +(:~ + : Sends a message to all clients, or to the clients of a specific user. + : @param $text text to be sent + : @param $to receiver of a private message (optional) + :) +declare function chat-util:message( + $text as xs:string, + $to as xs:string? +) as empty-sequence() { + let $ws-ids := ws:ids()[not($to) or ws:get(., $chat-util:id) = $to] + return ws:send({ + 'type': 'message', + 'text': serialize($text), + 'from': ws:get(ws:id(), $chat-util:id), + 'date': format-time(current-time(), '[H02]:[m02]:[s02]'), + 'private': boolean($to) + }, $ws-ids) +}; + +(:~ + : Closes all WebSocket connections from the specified user. + : @param $name username + :) +declare function chat-util:close( + $name as xs:string +) as empty-sequence() { + for $id in ws:ids() + where ws:get($id, $chat-util:id) = $name + return ws:close($id) +}; diff --git a/webapp/lsp/lsp-ws.xqm b/webapp/lsp/lsp-ws.xqm new file mode 100644 index 0000000..b5e0870 --- /dev/null +++ b/webapp/lsp/lsp-ws.xqm @@ -0,0 +1,46 @@ +(:~ + : Simple WebSocket chat. WebSocket functions. + : @author BaseX Team, BSD License + :) +module namespace chat-ws = 'chat-ws'; + +import module namespace chat-util = 'chat/util' at 'lsp-util.xqm'; +import module namespace request = "http://exquery.org/ns/request"; + +(:~ + : Creates a WebSocket connection. Registers the user and notifies all clients. + :) +declare + %ws:connect('/chat') +function chat-ws:connect() as empty-sequence() { + ws:set(ws:id(), $chat-util:id, session:get($chat-util:id)), + chat-util:users() +}; + +(:~ + : Processes a WebSocket message. + : @param $message message + :) +declare + %ws:message('/chat', '{$message}') +function chat-ws:message( + $message as xs:string +) as empty-sequence() { + let $json := parse-json($message) + let $type := $json?type + return if($type = 'message') then ( + chat-util:message($json?text, $json?to) + ) else if($type = 'ping') then( + (: do nothing :) + ) else error() +}; + +(:~ + : Closes a WebSocket connection. Unregisters the user and notifies all clients. + :) +declare + %ws:close('/chat') +function chat-ws:close() as empty-sequence() { + ws:delete(ws:id(), $chat-util:id), + chat-util:users() +}; diff --git a/webapp/lsp/lsp.xqm b/webapp/lsp/lsp.xqm new file mode 100644 index 0000000..90facf9 --- /dev/null +++ b/webapp/lsp/lsp.xqm @@ -0,0 +1,144 @@ +(:~ + : Simple WebSocket chat. RESTXQ functions. + : @author BaseX Team, BSD License + :) +module namespace chat = 'chat'; + +import module namespace chat-util = 'chat/util' at 'lsp-util.xqm'; + +(:~ + : Login or main page. + : @return HTML page + :) +declare + %rest:path('/lsp') + %output:method('html') +function chat:chat() as element() { + if(session:get($chat-util:id)) then ( + chat:main() + ) else ( + chat:login() + ) +}; + +(:~ + : Checks the user input, registers the user and reloads the chat. + : @param $name username + : @param $pass password + : @return redirection + :) +declare + %rest:POST + %rest:path('/lsp/login-check') + %rest:form-param('name', '{$name}') + %rest:form-param('pass', '{$pass}') +function chat:login-check( + $name as xs:string, + $pass as xs:string +) as element(rest:response) { + try { + user:check($name, $pass), + session:set($chat-util:id, $name) + } catch user:* { + (: login fails: no session info is set :) + }, + web:redirect('/chat') +}; + +(:~ + : Logs out the current user, notifies all WebSocket clients, and redirects to the login page. + : @return redirection + :) +declare + %rest:path('/lsp/logout') +function chat:logout() as element(rest:response) { + session:get($chat-util:id) ! chat-util:close(.), + session:delete($chat-util:id), + web:redirect('/chat') +}; + +(:~ + : Returns the HTML login page. + : @return HTML page + :) +declare %private function chat:login() as element(html) { + chat:wrap(( +
Please enter your credentials:
, +
+
+ + + + + + + + + +
Name: + +
Password:{ + , + + }
+ + ), ()) +}; + +(:~ + : Returns the HTML main page. + : @return HTML page + :) +declare %private function chat:main() as element(html) { + chat:wrap(( +

+ +

, + + + + + +
+
USERS (online)
+
+
+ +
CHAT MESSAGES
+
+
+ ),