From cbde740bd1032630c0f5811f61866d6d0a043a32 Mon Sep 17 00:00:00 2001 From: jmgomez Date: Fri, 30 Aug 2024 12:00:21 +0100 Subject: [PATCH] [green] initialize from the client should call initialized on the server --- ls.nim | 1 + lstransports.nim | 9 +-- nimlangserver.nim | 24 ++++---- tests/all.nim | 3 +- tests/tnimlangserver2.nim | 115 ++++++++++++++++++++++++++++++++++++++ tests/tsuggestapi.nim | 8 +-- 6 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 tests/tnimlangserver2.nim diff --git a/ls.nim b/ls.nim index b54dba6..883bf48 100644 --- a/ls.nim +++ b/ls.nim @@ -581,6 +581,7 @@ proc createOrRestartNimsuggest*(ls: LanguageServer, projectFile: string, uri = " ls.createOrRestartNimsuggest(projectFile, uri) ls.sendStatusChanged() errorCallback = proc (ns: Nimsuggest) {.gcsafe, raises: [].} = + debug "NimSuggest needed to be restarted due to an error " warn "Server stopped.", projectFile = projectFile if configuration.autoRestart.get(true) and ns.successfullCall: ls.createOrRestartNimsuggest(projectFile, uri) diff --git a/lstransports.nim b/lstransports.nim index b1bdb32..4b73441 100644 --- a/lstransports.nim +++ b/lstransports.nim @@ -130,12 +130,13 @@ proc readStdin2*(ctx: ptr ReadStdinContext) {.thread.} = ctx.value = processContentLength(inputStream) & CRLF discard ctx.onStdReadSignal.fireSync() discard ctx.onMainReadSignal.waitSync() - + +proc wrapContentWithContentLenght*(content: string): string = + let contentLenght = content.len + 1 + &"{CONTENT_LENGTH}{contentLenght}{CRLF}{CRLF}{content}\n" proc writeOutput*(ls: LanguageServer, content: JsonNode) = - let responseStr = $content - let contentLenght = responseStr.len + 1 - let res = &"{CONTENT_LENGTH}{contentLenght}{CRLF}{CRLF}{responseStr}\n" + let res = wrapContentWithContentLenght($content) try: case ls.transportMode: of stdio: diff --git a/nimlangserver.nim b/nimlangserver.nim index e70660c..dccd6e3 100644 --- a/nimlangserver.nim +++ b/nimlangserver.nim @@ -48,7 +48,7 @@ proc registerRoutes(srv: RpcSocketServer, ls: LanguageServer) = srv.register("$/setTrace", wrapRpc(partial(setTrace, ls))) -proc getNextFreePort(): Port= +proc getNextFreePort*(): Port= let s = newSocket() s.bindAddr(Port(0), "localhost") let (_, port) = s.getLocalAddr @@ -122,10 +122,8 @@ proc registerProcMonitor(ls: LanguageServer) = globalLS.stopNimsuggestProcessesP() exitnow(1) -proc main() = - let cmdLineParams = handleParams() +proc main*(cmdLineParams: CommandLineParams) = debug "Starting nimlangserver", params = cmdLineParams - #[ `nimlangserver` supports both transports: stdio and socket. By default it uses stdio transport. But we do construct a RPC socket server even in stdio mode, so that we can reuse the same code for both transports. @@ -140,11 +138,13 @@ proc main() = globalLS = addr ls #TODO use partial instead inside the func ls.srv.registerRoutes(ls) ls.registerProcMonitor() - - runForever() -try: - main() -except Exception as e: - error "Error in main" - writeStackTrace e - quit(1) + +when isMainModule: + try: + main(handleParams()) + runForever() + + except Exception as e: + error "Error in main" + writeStackTrace e + quit(1) diff --git a/tests/all.nim b/tests/all.nim index f1def8c..a4dd5b6 100644 --- a/tests/all.nim +++ b/tests/all.nim @@ -1,4 +1,5 @@ import # tnimlangserver, #These tests need to be redone - tsuggestapi + # tsuggestapi + tnimlangserver2 # tprojectsetup diff --git a/tests/tnimlangserver2.nim b/tests/tnimlangserver2.nim new file mode 100644 index 0000000..6ea5255 --- /dev/null +++ b/tests/tnimlangserver2.nim @@ -0,0 +1,115 @@ +import ../[ + nimlangserver, ls, lstransports +] +import ../protocol/types +import std/[options, unittest, json, os, jsonutils] +import json_rpc/[rpcclient] +import chronicles + + + +method call2*(client: RpcSocketClient, name: string, + params: JsonNode): Future[JsonString] {.async, gcsafe.} = + ## Remotely calls the specified RPC method. + let id = client.getNextId() + let reqJson = newJObject() + reqJson["jsonrpc"] = %"2.0" + reqJson["id"] = %id.num + reqJson["method"] = %name + reqJson["params"] = params + let reqContent = wrapContentWithContentLenght($reqJson) + # var jsonBytes = requestTxEncode(name, params, id) & "\r\n" + var jsonBytes = reqContent + echo "Sending ", jsonBytes + if client.transport.isNil: + raise newException(JsonRpcError, + "Transport is not initialised (missing a call to connect?)") + # completed by processMessage. + var newFut = newFuture[JsonString]() + # add to awaiting responses + client.awaiting[id] = newFut + + let res = await client.transport.write(jsonBytes) + # TODO: Add actions when not full packet was send, e.g. disconnect peer. + doAssert(res == jsonBytes.len) + + return await newFut + + +proc processData2(client: RpcSocketClient) {.async: (raises: []).} = + while true: + var localException: ref JsonRpcError + while true: + try: + # var value = await client.transport.readLine(defaultMaxRequestLength) + var value = await processContentLength(client.transport) + if value == "": + # transmission ends + await client.transport.closeWait() + break + + let res = client.processMessage(value) + if res.isErr: + error "Error when processing RPC message", msg=res.error + localException = newException(JsonRpcError, res.error) + break + except TransportError as exc: + localException = newException(JsonRpcError, exc.msg) + await client.transport.closeWait() + break + except CancelledError as exc: + localException = newException(JsonRpcError, exc.msg) + await client.transport.closeWait() + break + + if localException.isNil.not: + for _,fut in client.awaiting: + fut.fail(localException) + if client.batchFut.isNil.not and not client.batchFut.completed(): + client.batchFut.fail(localException) + + # async loop reconnection and waiting + try: + info "Reconnect to server", address=`$`(client.address) + client.transport = await connect(client.address) + except TransportError as exc: + error "Error when reconnecting to server", msg=exc.msg + break + except CancelledError as exc: + error "Error when reconnecting to server", msg=exc.msg + break + + + +proc onProcessMessage(client: RpcClient, line: string): Result[bool, string] {.gcsafe, raises: [].} = + # echo "onProcessmessage ", line #line contains the json response from the server. From here one could implement an actual client + return ok(true) + + +proc connect2*(client: RpcSocketClient, address: string, port: Port) {.async.} = + let addresses = resolveTAddress(address, port) + client.transport = await connect(addresses[0]) + client.address = addresses[0] + client.loop = processData2(client) + client.onProcessMessage = onProcessMessage + + +suite "Nimlangserver": + let cmdParams = CommandLineParams(transport: some socket, port: getNextFreePort()) + main(cmdParams) #we could accesss to the ls here to test against its state + let client = newRpcSocketClient() + waitFor client.connect2("localhost", cmdParams.port) + + test "initialize from the client should call initialized on the server": + let initParams = InitializeParams( + processId: some(%getCurrentProcessId()), + rootUri: some("file:///tmp/"), + capabilities: ClientCapabilities()) + let initializeResult = client.call2("initialize", %initParams).waitFor.string.parseJson.jsonTo(InitializeResult) + + check initializeResult.capabilities.textDocumentSync.isSome + + + + + diff --git a/tests/tsuggestapi.nim b/tests/tsuggestapi.nim index e4957c0..0c0545c 100644 --- a/tests/tsuggestapi.nim +++ b/tests/tsuggestapi.nim @@ -1,5 +1,5 @@ import - ../suggestapi, unittest, os, std/asyncnet, strutils, chronos + ../suggestapi, unittest, os, std/asyncnet, strutils, chronos, options const inputLine = "def skProc hw.a proc (){.noSideEffect, gcsafe.} hw/hw.nim 1 5 \"\" 100" const inputLineWithEndLine = "outline skEnumField system.bool.true bool basic_types.nim 46 15 \"\" 100 4 11" @@ -14,7 +14,7 @@ suite "Nimsuggest tests": doAssert parseQualifiedPath("system.`..<`") == @["system", "`..<`"] test "Parsing suggest": - doAssert parseSuggestDef(inputLine)[] == Suggest( + doAssert parseSuggestDef(inputLine).get[] == Suggest( filePath: "hw/hw.nim", qualifiedPath: @["hw", "a"], symKind: "skProc", @@ -25,8 +25,8 @@ suite "Nimsuggest tests": section: ideDef)[] test "Parsing suggest with endLine": - let res = parseSuggestDef(inputLineWithEndLine)[] - doAssert res == Suggest( + let res = parseSuggestDef(inputLineWithEndLine).get + doAssert res[] == Suggest( filePath: "basic_types.nim", qualifiedPath: @["system", "bool", "true"], symKind: "skEnumField",