diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9762303..d2e41d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Nimble uses: nim-lang/setup-nimble-action@v1 with: @@ -26,7 +26,7 @@ jobs: - name: Restore nimble dependencies from cache id: nimble_deps - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.nimble @@ -42,3 +42,16 @@ jobs: - name: Nimble Test shell: bash run: nimble test + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `nph` formatting + uses: arnetheduck/nph-action@v1 + with: + version: 0.6.2 + options: "*.nim protocol" + fail: true + suggest: true diff --git a/asyncprocmonitor.nim b/asyncprocmonitor.nim index 7fe3b98..b84ee27 100644 --- a/asyncprocmonitor.nim +++ b/asyncprocmonitor.nim @@ -7,7 +7,7 @@ when defined(posix): import posix_utils import posix -type Callback* = proc() {.closure, gcsafe, raises: [].} +type Callback* = proc() {.gcsafe, raises: [].} when defined(windows): import winlean, sugar diff --git a/ls.nim b/ls.nim index 4197049..caf122a 100644 --- a/ls.nim +++ b/ls.nim @@ -160,7 +160,8 @@ type nimDumpCache*: Table[string, NimbleDumpInfo] #path to NimbleDumpInfo entryPoints*: seq[string] responseMap*: TableRef[string, Future[JsonNode]] - testRunProcess*: Option[AsyncProcessRef] #There is only one test run process at a time + testRunProcess*: Option[AsyncProcessRef] + #There is only one test run process at a time #id to future. Represents the pending requests as result of calling ls.call srv*: RpcSocketServer @@ -178,7 +179,7 @@ type failTable*: Table[string, int] #Project file to fail count #List of errors (crashes) nimsuggest has had since the lsp session started - checkInProgress*: bool + checkInProgress*: bool Certainty* = enum None @@ -256,7 +257,9 @@ proc supportSignatureHelp*(cc: ClientCapabilities): bool = let caps = cc.textDocument caps.isSome and caps.get.signatureHelp.isSome -proc getNimbleDumpInfo*(ls: LanguageServer, nimbleFile: string): Future[NimbleDumpInfo] {.async.}= +proc getNimbleDumpInfo*( + ls: LanguageServer, nimbleFile: string +): Future[NimbleDumpInfo] {.async.} = if nimbleFile in ls.nimDumpCache: return ls.nimDumpCache.getOrDefault(nimbleFile) var process: AsyncProcessRef @@ -290,10 +293,9 @@ proc getNimbleDumpInfo*(ls: LanguageServer, nimbleFile: string): Future[NimbleDu ls.nimDumpCache[nimbleFile] = result except OSError, IOError: debug "Failed to get nimble dump info", nimbleFile = nimbleFile - finally: + finally: await shutdownChildProcess(process) - proc parseWorkspaceConfiguration*(conf: JsonNode): NlsConfig = try: if conf.kind == JObject and conf["settings"].kind == JObject: @@ -487,7 +489,7 @@ proc getNimVersion(nimDir: string): string = proc getNimSuggestPathAndVersion( ls: LanguageServer, conf: NlsConfig, workingDir: string -): Future[(string, string)] {.async.}= +): Future[(string, string)] {.async.} = #Attempting to see if the project is using a custom Nim version, if it's the case this will be slower than usual let nimbleDumpInfo = await ls.getNimbleDumpInfo("") let nimDir = nimbleDumpInfo.nimDir.get "" @@ -518,7 +520,9 @@ proc getNimPath*(conf: NlsConfig): Option[string] = warn "Failed to find nim path" none(string) -proc getProjectFileAutoGuess*(ls: LanguageServer, fileUri: string): Future[string] {.async.}= +proc getProjectFileAutoGuess*( + ls: LanguageServer, fileUri: string +): Future[string] {.async.} = let file = fileUri.decodeUrl debug "Auto-guessing project file for", file = file result = file @@ -742,7 +746,7 @@ proc sendDiagnostics*( proc warnIfUnknown*( ls: LanguageServer, ns: Nimsuggest, uri: string, projectFile: string -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = let path = uri.uriToPath let isFileKnown = await ns.isKnown(path) if not isFileKnown and not ns.canHandleUnknown: @@ -799,7 +803,7 @@ proc tryGetNimsuggest*( proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.raises: [], gcsafe.} -proc didCloseFile*(ls: LanguageServer, uri: string): Future[void] {.async, gcsafe.} = +proc didCloseFile*(ls: LanguageServer, uri: string): Future[void] {.async.} = debug "Closed the following document:", uri = uri if ls.openFiles[uri].changed: @@ -808,22 +812,18 @@ proc didCloseFile*(ls: LanguageServer, uri: string): Future[void] {.async, gcsaf ls.openFiles.del uri -proc makeIdleFile*( - ls: LanguageServer, file: NlsFileInfo -): Future[void] {.async, gcsafe.} = +proc makeIdleFile*(ls: LanguageServer, file: NlsFileInfo): Future[void] {.async.} = let uri = file.textDocument.uri if uri in ls.openFiles: await ls.didCloseFile(uri) ls.idleOpenFiles[uri] = file ls.openFiles.del(uri) -proc getProjectFile*( - fileUri: string, ls: LanguageServer -): Future[string] {.raises: [], gcsafe.} +proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.async.} proc didOpenFile*( ls: LanguageServer, textDocument: TextDocumentItem -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = with textDocument: debug "New document opened for URI:", uri = uri let @@ -891,11 +891,12 @@ proc tryGetNimsuggest*( debug "Nimsuggest not found after retries", uri = uri return none(NimSuggest) -proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsafe.} = +proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async.} = if ls.checkInProgress: return ls.checkInProgress = true - defer: ls.checkInProgress = false + defer: + ls.checkInProgress = false if not ls.getWorkspaceConfiguration.await().autoCheckProject.get(true): return @@ -950,7 +951,7 @@ proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsaf ls.workDoneProgressCreate(token) ls.progress(token, "begin", fmt "Checking project {uri.uriToPath}") nimsuggest.checkProjectInProgress = true - defer: + defer: nimsuggest.checkProjectInProgress = false ls.progress(token, "end") @@ -1021,7 +1022,8 @@ proc createOrRestartNimsuggest*( let configuration = ls.getWorkspaceConfiguration().waitFor() workingDir = ls.getWorkingDir(projectFile).waitFor() - (nimsuggestPath, version) = ls.getNimSuggestPathAndVersion(configuration, workingDir).waitFor() + (nimsuggestPath, version) = + ls.getNimSuggestPathAndVersion(configuration, workingDir).waitFor() timeout = configuration.timeout.get(REQUEST_TIMEOUT) restartCallback = proc(ns: Nimsuggest) {.gcsafe, raises: [].} = warn "Restarting the server due to requests being to slow", @@ -1089,7 +1091,9 @@ proc maybeRegisterCapabilityDidChangeConfiguration*(ls: LanguageServer) = ) ls.didChangeConfigurationRegistrationRequest = ls.call("client/registerCapability", %registrationParams) - ls.didChangeConfigurationRegistrationRequest.addCallback do(res: Future[JsonNode]): + ls.didChangeConfigurationRegistrationRequest.addCallback do(res: Future[JsonNode]) {. + gcsafe + .}: debug "Got response for the didChangeConfiguration registration:", res = res.read() @@ -1184,7 +1188,8 @@ proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.asyn let shouldSpawn = await ls.shouldSpawnNimsuggest() if not shouldSpawn: result = ls.projectFiles.keys.toSeq[0] - debug "Reached the maximum instances of nimsuggest, reusing the first nimsuggest instance", project = result + debug "Reached the maximum instances of nimsuggest, reusing the first nimsuggest instance", + project = result return result result = await ls.getProjectFileAutoGuess(fileUri) diff --git a/lstransports.nim b/lstransports.nim index 6f8ff7b..5fc828e 100644 --- a/lstransports.nim +++ b/lstransports.nim @@ -286,6 +286,7 @@ proc startSocketServer*(ls: LanguageServer, port: Port) = return while ls.socketTransport.isNil: await sleepAsync(0) + debug "Waiting for socket server to be ready" waitFor waitUntilSocketTransportIsReady(ls) debug "Socket server started" diff --git a/nimcheck.nim b/nimcheck.nim index da8a01d..4b353a6 100644 --- a/nimcheck.nim +++ b/nimcheck.nim @@ -27,14 +27,16 @@ proc parseCheckResults(lines: seq[string]): seq[CheckResult] = stacktrace: seq[CheckStacktrace] lastFile, lastLineStr, lastCharStr: string m: RegexMatch2 - + let dotsPattern = re2"^\.+$" let errorPattern = re2"^([^(]+)\((\d+),\s*(\d+)\)\s*(\w+):\s*(.*)$" - + for line in lines: let line = line.strip() - if line.startsWith("Hint: used config file") or line == "" or line.match(dotsPattern): + if line.startsWith("Hint: used config file") or line == "" or line.match( + dotsPattern + ): continue if not find(line, errorPattern, m): @@ -48,24 +50,25 @@ proc parseCheckResults(lines: seq[string]): seq[CheckResult] = charStr = line[m.captures[2]] severity = line[m.captures[3]] msg = line[m.captures[4]] - - let + + let lineNum = parseInt(lineStr) colNum = parseInt(charStr) - result.add(CheckResult( - file: file, - line: lineNum, - column: colNum, - msg: msg, - severity: severity, - stacktrace: @[] - )) - + result.add( + CheckResult( + file: file, + line: lineNum, + column: colNum, + msg: msg, + severity: severity, + stacktrace: @[], + ) + ) except Exception as e: error "Error processing line", line = line, msg = e.msg continue - + if messageText.len > 0 and result.len > 0: result[^1].msg &= "\n" & messageText @@ -89,13 +92,13 @@ proc nimCheck*(filePath: string, nimPath: string): Future[seq[CheckResult]] {.as let res = await process.waitForExit(15.seconds) # debug "nimCheck exit", res = res var output = "" - if res == 0: #Nim check return 0 if there are no errors but we still need to check for hints and warnings - output = string.fromBytes(process.stdoutStream.read().await) + if res == 0: + #Nim check return 0 if there are no errors but we still need to check for hints and warnings + output = string.fromBytes(process.stdoutStream.read().await) else: output = string.fromBytes(process.stderrStream.read().await) - + let lines = output.splitLines() parseCheckResults(lines) - finally: await shutdownChildProcess(process) diff --git a/nimexpand.nim b/nimexpand.nim index b0a3137..976f2c7 100644 --- a/nimexpand.nim +++ b/nimexpand.nim @@ -6,11 +6,11 @@ import utils import suggestapi import std/[strformat] - proc extractMacroExpansion*(output: string, targetLine: int): string = var start = false for line in output.split({'\n', '\r'}): - if line.len == 0: continue + if line.len == 0: + continue debug "extractMacroExpansion", line = line, condMet = &".nim({targetLine}," in line if &".nim({targetLine}," in line: start = true @@ -18,15 +18,17 @@ proc extractMacroExpansion*(output: string, targetLine: int): string = break if start: result.add line & "\n" - + if result.len > 0: let macroStart = result.find("macro: ") if macroStart != -1: result = result.substr(macroStart + "macro: ".len) result = result.replace("[ExpandMacro]", "") -proc nimExpandMacro*(nimPath: string, suggest: Suggest, filePath: string): Future[string] {.async.} = - let +proc nimExpandMacro*( + nimPath: string, suggest: Suggest, filePath: string +): Future[string] {.async.} = + let macroName = suggest.qualifiedPath[suggest.qualifiedPath.len - 1] line = suggest.line debug "nimExpandMacro", macroName = macroName, line = line, filePath = filePath @@ -39,12 +41,11 @@ proc nimExpandMacro*(nimPath: string, suggest: Suggest, filePath: string): Futur ) try: let res = await process.waitForExit(10.seconds) - let output = string.fromBytes(process.stdoutStream.read().await) + let output = string.fromBytes(process.stdoutStream.read().await) result = extractMacroExpansion(output, line) finally: await shutdownChildProcess(process) - proc extractArcExpansion*(output: string, procName: string): string = var start = false let cond = &"--expandArc: {procName}" @@ -56,12 +57,13 @@ proc extractArcExpansion*(output: string, procName: string): string = break if start: result.add line & "\n" - + if result.len > 0: result = result.replace(cond, "").strip() - -proc nimExpandArc*(nimPath: string, suggest: Suggest, filePath: string): Future[string] {.async.} = +proc nimExpandArc*( + nimPath: string, suggest: Suggest, filePath: string +): Future[string] {.async.} = let procName = suggest.qualifiedPath[suggest.qualifiedPath.len - 1] debug "nimExpandArc", procName = procName, filePath = filePath let process = await startProcess( @@ -72,11 +74,11 @@ proc nimExpandArc*(nimPath: string, suggest: Suggest, filePath: string): Future[ ) try: let res = await process.waitForExit(10.seconds) - let output = string.fromBytes(process.stdoutStream.read().await) + let output = string.fromBytes(process.stdoutStream.read().await) result = extractArcExpansion(output, procName) # debug "nimExpandArc", output = output, result = result if result.len == 0: result = &"#Couldnt expand arc for `{procName}`. Showing raw output instead \n" - result.add output + result.add output finally: - await shutdownChildProcess(process) \ No newline at end of file + await shutdownChildProcess(process) diff --git a/nimlangserver.nim b/nimlangserver.nim index e146a71..2de267c 100644 --- a/nimlangserver.nim +++ b/nimlangserver.nim @@ -79,7 +79,9 @@ proc registerRoutes(srv: RpcSocketServer, ls: LanguageServer) = "workspace/didChangeConfiguration", wrapRpc(partial(didChangeConfiguration, ls)) ) srv.register("textDocument/didChange", wrapRpc(partial(didChange, ls))) - srv.register("textDocument/willSaveWaitUntil", wrapRpc(partial(willSaveWaitUntil, ls))) + srv.register( + "textDocument/willSaveWaitUntil", wrapRpc(partial(willSaveWaitUntil, ls)) + ) srv.register("$/setTrace", wrapRpc(partial(setTrace, ls))) proc showHelp() = @@ -104,17 +106,14 @@ proc showHelp() = if line.startsWith("```"): inCodeBlock = not inCodeBlock continue - + var cleaned = line if not inCodeBlock: - cleaned = cleaned.multiReplace([ - ("`", ""), - ("\\", "") - ]) - + cleaned = cleaned.multiReplace([("`", ""), ("\\", "")]) + if cleaned.len > 0: result.add(cleaned & "\n") - + result = result.strip() let section = readme.split("## Configuration Options")[1].split("##")[0] diff --git a/protocol/types.nim b/protocol/types.nim index d77e312..8146eb3 100644 --- a/protocol/types.nim +++ b/protocol/types.nim @@ -1003,8 +1003,8 @@ type LspExtensionCapability* = enum #List of extensions this server support. Useful for clients - excRestartSuggest = "RestartSuggest", - excNimbleTask = "NimbleTask", + excRestartSuggest = "RestartSuggest" + excNimbleTask = "NimbleTask" excRunTests = "RunTests" ProjectError* = object @@ -1036,21 +1036,21 @@ type actionPerformed*: SuggestAction NimbleTask* = object - name*: string + name*: string description*: string RunTaskParams* = object command*: seq[string] #command and args - + RunTaskResult* = object command*: seq[string] #command and args output*: seq[string] #output lines - + TestInfo* = object name*: string line*: int file*: string - + TestSuiteInfo* = object name*: string #The suite name, empty if it's a global test tests*: seq[TestInfo] @@ -1061,11 +1061,12 @@ type error*: Option[string] ListTestsParams* = object - entryPoint*: string #can be patterns? if empty we could do the same as nimble does or just run `nimble test args` + entryPoint*: string + #can be patterns? if empty we could do the same as nimble does or just run `nimble test args` ListTestsResult* = object projectInfo*: TestProjectInfo - + RunTestResult* = object name*: string time*: float @@ -1079,10 +1080,11 @@ type skipped*: int time*: float testResults*: seq[RunTestResult] - + RunTestParams* = object entryPoint*: string - suiteName*: Option[string] #Optional, if provided, only run tests in the suite. Takes precedence over testName + suiteName*: Option[string] + #Optional, if provided, only run tests in the suite. Takes precedence over testName testNames*: Option[seq[string]] #Optional, if provided, only run the specific tests RunTestProjectResult* = object diff --git a/routes.nim b/routes.nim index 4a2af47..64fee9a 100644 --- a/routes.nim +++ b/routes.nim @@ -22,7 +22,7 @@ import stew/[byteutils], nimexpand, testrunner - + import macros except error proc getNphPath(): Option[string] = @@ -373,7 +373,7 @@ proc toMdLinks(s: string): string = for i in countDown(matches.high, matches.low): let match = matches[i] result[match.boundaries] = fmt"[{s[match.captures[0]]}]({s[match.captures[1]]})" - + proc toMarkupContent(suggest: Suggest): MarkupContent = result = MarkupContent(kind: "markdown", value: "```nim\n") result.value.add suggest.qualifiedPath.join(".") @@ -398,8 +398,9 @@ proc hover*( let ch = ls.getCharacter(uri, line, character) if ch.isNone: return none(Hover) - let suggestions = - await nimsuggest.get().highlight(uriToPath(uri), ls.uriToStash(uri), line + 1, ch.get) + let suggestions = await nimsuggest.get().highlight( + uriToPath(uri), ls.uriToStash(uri), line + 1, ch.get + ) if suggestions.len == 0: return none(Hover) var suggest = suggestions[0] @@ -413,29 +414,33 @@ proc hover*( else: break var content = toMarkupContent(suggest) - if suggest.symkind == "skMacro" and config.nimExpandMacro.get(NIM_EXPAND_MACRO_BY_DEFAULT): - let expanded = await nimsuggest.get - .expand(uriToPath(uri), ls.uriToStash(uri), suggest.line, suggest.column) + if suggest.symkind == "skMacro" and + config.nimExpandMacro.get(NIM_EXPAND_MACRO_BY_DEFAULT): + let expanded = await nimsuggest.get.expand( + uriToPath(uri), ls.uriToStash(uri), suggest.line, suggest.column + ) if expanded.len > 0 and expanded[0].doc != "": # debug "Expanded macro", expanded = expanded[0].doc content.value.add &"```nim\n{expanded[0].doc}\n```" - else: + else: # debug "Couldnt expand the macro. Trying with nim expand", suggest = suggest[] let nimPath = config.getNimPath() - if nimPath.isSome: + if nimPath.isSome: let expanded = await nimExpandMacro(nimPath.get, suggest, uriToPath(uri)) content.value.add &"```nim\n{expanded}\n```" - if suggest.section == ideDef and suggest.symkind in ["skProc"] and config.nimExpandArc.get(NIM_EXPAND_ARC_BY_DEFAULT): + if suggest.section == ideDef and suggest.symkind in ["skProc"] and + config.nimExpandArc.get(NIM_EXPAND_ARC_BY_DEFAULT): debug "#Expanding arc", suggest = suggest[] let nimPath = config.getNimPath() - if nimPath.isSome: + if nimPath.isSome: let expanded = await nimExpandArc(nimPath.get, suggest, uriToPath(uri)) let arcContent = "#Expanded arc \n" & expanded content.value.add &"```nim\n{arcContent}\n```" - return some(Hover( - contents: some(%content), - range: some(toLabelRange(suggest.toUtf16Pos(ls))), - )) + return some( + Hover( + contents: some(%content), range: some(toLabelRange(suggest.toUtf16Pos(ls))) + ) + ) proc references*( ls: LanguageServer, params: ReferenceParams @@ -746,7 +751,7 @@ proc format*( let fullRange = Range( start: Position(line: 0, character: 0), - `end`: Position(line: uinteger.high, character: uinteger.high) + `end`: Position(line: uinteger.high, character: uinteger.high), ) debug "Formatting document", uri = uri, formattedText = formattedText some TextEdit(range: fullRange, newText: formattedText) @@ -795,9 +800,7 @@ proc extractId(id: JsonNode): int = if id.kind == JString: discard parseInt(id.getStr, result) -proc shutdown*( - ls: LanguageServer, input: JsonNode -): Future[JsonNode] {.async, gcsafe.} = +proc shutdown*(ls: LanguageServer, input: JsonNode): Future[JsonNode] {.async.} = debug "Shutting down" await ls.stopNimsuggestProcesses() ls.isShutdown = true @@ -807,7 +810,7 @@ proc shutdown*( proc exit*( p: tuple[ls: LanguageServer, onExit: OnExitCallback], _: JsonNode -): Future[JsonNode] {.async, gcsafe.} = +): Future[JsonNode] {.async.} = if not p.ls.isShutdown: debug "Received an exit request without prior shutdown request" await p.ls.stopNimsuggestProcesses() @@ -815,7 +818,9 @@ proc exit*( result = newJNull() await p.onExit() -proc startNimbleProcess(ls: LanguageServer, args: seq[string]): Future[AsyncProcessRef] {.async.} = +proc startNimbleProcess( + ls: LanguageServer, args: seq[string] +): Future[AsyncProcessRef] {.async.} = await startProcess( "nimble", arguments = args, @@ -825,9 +830,7 @@ proc startNimbleProcess(ls: LanguageServer, args: seq[string]): Future[AsyncProc stderrHandle = AsyncProcess.Pipe, ) -proc tasks*( - ls: LanguageServer, conf: JsonNode -): Future[seq[NimbleTask]] {.async.} = +proc tasks*(ls: LanguageServer, conf: JsonNode): Future[seq[NimbleTask]] {.async.} = let rootPath: string = ls.initializeParams.getRootPath debug "Received tasks ", rootPath = rootPath delEnv "NIMBLE_DIR" @@ -846,7 +849,7 @@ proc tasks*( proc runTask*( ls: LanguageServer, params: RunTaskParams ): Future[RunTaskResult] {.async.} = - let process = await ls.startNimbleProcess(params.command) + let process = await ls.startNimbleProcess(params.command) let res = await process.waitForExit(InfiniteDuration) result.command = params.command let prefix = "\"" @@ -855,9 +858,9 @@ proc runTask*( for line in lines.mitems: if line.startsWith(prefix): line = line.unescape(prefix) - if line != "": - result.output.add line - + if line != "": + result.output.add line + debug "Ran nimble cmd/task", command = $params.command, output = $result.output await process.shutdownChildProcess() @@ -868,7 +871,11 @@ proc listTests*( let nimPath = config.getNimPath() if nimPath.isNone: error "Nim path not found when listing tests" - return ListTestsResult(projectInfo: TestProjectInfo(entryPoint: params.entryPoint, suites: initTable[string, TestSuiteInfo]())) + return ListTestsResult( + projectInfo: TestProjectInfo( + entryPoint: params.entryPoint, suites: initTable[string, TestSuiteInfo]() + ) + ) let workspaceRoot = ls.initializeParams.getRootPath let testProjectInfo = await listTests(params.entryPoint, nimPath.get(), workspaceRoot) result.projectInfo = testProjectInfo @@ -882,13 +889,21 @@ proc runTests*( error "Nim path not found when running tests" return RunTestProjectResult() let workspaceRoot = ls.initializeParams.getRootPath - await runTests(params.entryPoint, nimPath.get(), params.suiteName, params.testNames.get(@[]), workspaceRoot, ls) + await runTests( + params.entryPoint, + nimPath.get(), + params.suiteName, + params.testNames.get(@[]), + workspaceRoot, + ls, + ) proc cancelTest*( ls: LanguageServer, params: JsonNode ): Future[CancelTestResult] {.async.} = debug "Cancelling test" - if ls.testRunProcess.isSome: #No need to cancel the runTests request. The client should handle it. + if ls.testRunProcess.isSome: + #No need to cancel the runTests request. The client should handle it. await shutdownChildProcess(ls.testRunProcess.get) ls.testRunProcess = none(AsyncProcessRef) CancelTestResult(cancelled: true) @@ -918,7 +933,7 @@ proc setTrace*(ls: LanguageServer, params: SetTraceParams) {.async.} = proc didChange*( ls: LanguageServer, params: DidChangeTextDocumentParams -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = with params: let uri = textDocument.uri if uri notin ls.openFiles: @@ -938,8 +953,7 @@ proc didChange*( ls.scheduleFileCheck(uri) proc willSaveWaitUntil*( - ls: LanguageServer, - params: WillSaveTextDocumentParams + ls: LanguageServer, params: WillSaveTextDocumentParams ): Future[seq[TextEdit]] {.async.} = debug "Received willSaveWaitUntil request" @@ -947,23 +961,22 @@ proc willSaveWaitUntil*( uri = params.textDocument.uri config = await ls.getWorkspaceConfiguration() nphPath = getNphPath() - - let shouldFormat = - nphPath.isSome and - ls.serverCapabilities.documentFormattingProvider.get(false) and + + let shouldFormat = + nphPath.isSome and ls.serverCapabilities.documentFormattingProvider.get(false) and config.formatOnSave.get(false) - + if shouldFormat: debug "Formatting document before save", uri = uri let formatTextEdit = await ls.format(nphPath.get(), uri) if formatTextEdit.isSome: return @[formatTextEdit.get] - + return @[] proc didSave*( ls: LanguageServer, params: DidSaveTextDocumentParams -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = let uri = params.textDocument.uri config = await ls.getWorkspaceConfiguration() @@ -999,17 +1012,17 @@ proc didSave*( proc didClose*( ls: LanguageServer, params: DidCloseTextDocumentParams -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = await ls.didCloseFile(params.textDocument.uri) proc didOpen*( ls: LanguageServer, params: DidOpenTextDocumentParams -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = await ls.didOpenFile(params.textDocument) proc didChangeConfiguration*( ls: LanguageServer, conf: JsonNode -): Future[void] {.async, gcsafe.} = +): Future[void] {.async.} = debug "Changed configuration: ", conf = conf if ls.usePullConfigurationModel: ls.maybeRequestConfigurationFromClient diff --git a/suggestapi.nim b/suggestapi.nim index 4b2bcf2..c898198 100644 --- a/suggestapi.nim +++ b/suggestapi.nim @@ -343,7 +343,7 @@ proc createNimsuggest*( workingDir = getCurrentDir(), enableLog: bool = false, enableExceptionInlayHints: bool = false, -): Future[Project] {.async, gcsafe.} = +): Future[Project] {.async.} = result = Project(file: root) result.ns = newFuture[NimSuggest]() result.errorCallback = some errorCallback @@ -400,7 +400,7 @@ proc createNimsuggest*( ns.port = portLine.parseInt except ValueError: error "Failed to parse nimsuggest port", portLine = portLine - let nextLine = await result.process.stdoutStream.readLine(sep = "\n") + let nextLine = await result.process.stdoutStream.readLine(sep = "\n") error "Nimsuggest nextLine", nextLine = nextLine result.markFailed "Failed to parse nimsuggest port" result.ns.complete(ns) @@ -431,7 +431,7 @@ proc processQueue(self: Nimsuggest): Future[void] {.async.} = while self.requestQueue.len != 0: let req = self.requestQueue.popFirst self.project.lastCmd = req.commandString - + logScope: command = req.commandString if req.future.finished: diff --git a/testrunner.nim b/testrunner.nim index 8483ded..693ba13 100644 --- a/testrunner.nim +++ b/testrunner.nim @@ -1,4 +1,6 @@ -import std/[os, strscans, tables, enumerate, strutils, xmlparser, xmltree, options, strformat] +import + std/ + [os, strscans, tables, enumerate, strutils, xmlparser, xmltree, options, strformat] import chronos, chronos/asyncproc import protocol/types import ls @@ -9,23 +11,23 @@ import utils proc extractTestInfo*(rawOutput: string): TestProjectInfo = result.suites = initTable[string, TestSuiteInfo]() let lines = rawOutput.split("\n") - var currentSuite = "" + var currentSuite = "" for i, line in enumerate(lines): var name, file, ignore: string var lineNumber: int if scanf(line, "Suite: $*", name): - currentSuite = name.strip() + currentSuite = name.strip() result.suites[currentSuite] = TestSuiteInfo(name: currentSuite) # echo "Found suite: ", currentSuite - elif scanf(line, "$*Test: $*", ignore, name): let insideSuite = line.startsWith("\t") # Use currentSuite if inside a suite, empty string if not let suiteName = if insideSuite: currentSuite else: "" - + #File is always next line of a test - if scanf(lines[i+1], "$*File:$*:$i", ignore, file, lineNumber): - var testInfo = TestInfo(name: name.strip(), file: file.strip(), line: lineNumber) + if scanf(lines[i + 1], "$*File:$*:$i", ignore, file, lineNumber): + var testInfo = + TestInfo(name: name.strip(), file: file.strip(), line: lineNumber) # echo "Adding test: ", testInfo.name, " to suite: ", suiteName result.suites[suiteName].tests.add(testInfo) @@ -36,7 +38,7 @@ proc getFullPath*(entryPoint: string, workspaceRoot: string): string = return absolutePath return entryPoint -proc parseObject(obj: var object, node: XmlNode) = +proc parseObject(obj: var object, node: XmlNode) = for field, value in obj.fieldPairs: when value is string: getField(obj, field) = node.attr(field) @@ -53,12 +55,12 @@ proc parseTestResult*(node: XmlNode): RunTestResult = result.failure = some failureNode.attr("message") proc parseTestSuite*(node: XmlNode): RunTestSuiteResult = - parseObject(result, node) + parseObject(result, node) for testCase in node.findAll("testcase"): result.testResults.add(parseTestResult(testCase)) proc parseTestResults*(xmlContent: string): RunTestProjectResult = - let xml = parseXml(xmlContent) + let xml = parseXml(xmlContent) for suiteNode in xml.findAll("testsuite"): let suite = parseTestSuite(suiteNode) # echo suite.name, " ", suite.testResults.len @@ -66,14 +68,13 @@ proc parseTestResults*(xmlContent: string): RunTestProjectResult = result.suites.add(suite) proc listTests*( - entryPoint: string, - nimPath: string, - workspaceRoot: string + entryPoint: string, nimPath: string, workspaceRoot: string ): Future[TestProjectInfo] {.async.} = var entryPoint = getFullPath(entryPoint, workspaceRoot) let executableDir = (getTempDir() / entryPoint.splitFile.name).absolutePath debug "Listing tests", entryPoint = entryPoint, exists = fileExists(entryPoint) - let args = @["c", "--outdir:" & executableDir, "-d:unittest2ListTests", "-r", entryPoint] + let args = + @["c", "--outdir:" & executableDir, "-d:unittest2ListTests", "-r", entryPoint] let process = await startProcess( nimPath, arguments = args, @@ -86,12 +87,13 @@ proc listTests*( if res != 0: result = extractTestInfo(error) if result.suites.len == 0: - error "Failed to list tests", nimPath = nimPath, entryPoint = entryPoint, res = res + error "Failed to list tests", + nimPath = nimPath, entryPoint = entryPoint, res = res error "An error occurred while listing tests" for line in error.splitLines: error "Error line: ", line = line error "Command args: ", args = args - result = TestProjectInfo(error: some error) + result = TestProjectInfo(error: some error) else: let rawOutput = await process.stdoutStream.readAllOutput() debug "list test raw output", rawOutput = rawOutput @@ -100,21 +102,22 @@ proc listTests*( await shutdownChildProcess(process) proc runTests*( - entryPoint: string, - nimPath: string, - suiteName: Option[string], - testNames: seq[string], - workspaceRoot: string, - ls: LanguageServer + entryPoint: string, + nimPath: string, + suiteName: Option[string], + testNames: seq[string], + workspaceRoot: string, + ls: LanguageServer, ): Future[RunTestProjectResult] {.async.} = var entryPoint = getFullPath(entryPoint, workspaceRoot) if not fileExists(entryPoint): - error "Entry point does not exist", entryPoint = entryPoint + error "Entry point does not exist", entryPoint = entryPoint return RunTestProjectResult() - let resultFile = (getTempDir() / "result.xml").absolutePath + let resultFile = (getTempDir() / "result.xml").absolutePath removeFile(resultFile) let executableDir = (getTempDir() / entryPoint.splitFile.name).absolutePath - var args = @["c", "--outdir:" & executableDir, "-r", entryPoint , fmt"--xml:{resultFile}"] + var args = + @["c", "--outdir:" & executableDir, "-r", entryPoint, fmt"--xml:{resultFile}"] if suiteName.isSome: args.add(fmt"{suiteName.get()}::") else: @@ -137,7 +140,7 @@ proc runTests*( result.fullOutput = error return result - error "Failed to run tests", nimPath = nimPath, entryPoint = entryPoint, res = res + error "Failed to run tests", nimPath = nimPath, entryPoint = entryPoint, res = res error "An error occurred while running tests" error "Error from process", error = error result = RunTestProjectResult(fullOutput: error) @@ -148,7 +151,6 @@ proc runTests*( # echo "XML CONTENT: ", xmlContent result = parseTestResults(xmlContent) result.fullOutput = output - except Exception as e: let processOutput = string.fromBytes(process.stdoutStream.read().await) error "An error occurred while running tests", error = e.msg @@ -157,4 +159,4 @@ proc runTests*( removeFile(resultFile) await shutdownChildProcess(process) if ls.testRunProcess.isSome: - ls.testRunProcess = none(AsyncProcessRef) \ No newline at end of file + ls.testRunProcess = none(AsyncProcessRef) diff --git a/tests/lspsocketclient.nim b/tests/lspsocketclient.nim index 8d91c74..65858f3 100644 --- a/tests/lspsocketclient.nim +++ b/tests/lspsocketclient.nim @@ -1,6 +1,4 @@ -import ../[ - ls, lstransports, utils -] +import ../[ls, lstransports, utils] import ../protocol/types import std/[options, unittest, json, os, jsonutils, tables, strutils, sequtils, sugar] @@ -11,26 +9,29 @@ import chronicles proc fixtureUri*(path: string): string = result = pathToUri(getCurrentDir() / "tests" / path) -type - NotificationRpc* = proc (params: JsonNode): Future[void] {.gcsafe, raises:[].} - Rpc* = proc (params: JsonNode): Future[JsonNode] {.gcsafe, raises:[].} +type + NotificationRpc* = proc(params: JsonNode): Future[void] {.async.} + Rpc* = proc(params: JsonNode): Future[JsonNode] {.async.} LspSocketClient* = ref object of RpcSocketClient notifications*: TableRef[string, NotificationRpc] routes*: TableRef[string, Rpc] - calls*: TableRef[string, seq[JsonNode]] #Stores all requests here from the server so we can test on them - responses*: TableRef[int, Future[JsonNode]] #id -> response. Stores the responses to the calls + calls*: TableRef[string, seq[JsonNode]] + #Stores all requests here from the server so we can test on them + responses*: TableRef[int, Future[JsonNode]] + #id -> response. Stores the responses to the calls -proc newLspSocketClient*(): LspSocketClient = +proc newLspSocketClient*(): LspSocketClient = result = LspSocketClient.new() result.routes = newTable[string, Rpc]() result.notifications = newTable[string, NotificationRpc]() result.calls = newTable[string, seq[JsonNode]]() result.responses = newTable[int, Future[JsonNode]]() -method call*(client: LspSocketClient, name: string, - params: JsonNode): Future[JsonNode] {.async, gcsafe.} = +method call*( + client: LspSocketClient, name: string, params: JsonNode +): Future[JsonNode] {.async.} = ## Remotely calls the specified RPC method. - let id = client.getNextId() + let id = client.getNextId() let reqJson = newJObject() reqJson["jsonrpc"] = %"2.0" reqJson["id"] = %id.num @@ -39,8 +40,9 @@ method call*(client: LspSocketClient, name: string, let reqContent = wrapContentWithContentLenght($reqJson) var jsonBytes = reqContent if client.transport.isNil: - raise newException(JsonRpcError, - "Transport is not initialised (missing a call to connect?)") + raise newException( + JsonRpcError, "Transport is not initialised (missing a call to connect?)" + ) # completed by processData. var newFut = newFuture[JsonNode]() # add to awaiting responses @@ -48,7 +50,7 @@ method call*(client: LspSocketClient, name: string, let res = await client.transport.write(jsonBytes) return await newFut -proc runRpc(client: LspSocketClient, rpc: Rpc, serverReq: JsonNode) {.async.} = +proc runRpc(client: LspSocketClient, rpc: Rpc, serverReq: JsonNode) {.async.} = let res = await rpc(serverReq["params"]) let id = serverReq["id"].jsonTo(string) let reqJson = newJObject() @@ -58,15 +60,15 @@ proc runRpc(client: LspSocketClient, rpc: Rpc, serverReq: JsonNode) {.async.} = let reqContent = wrapContentWithContentLenght($reqJson) discard await client.transport.write(reqContent.string) -proc processMessage(client: LspSocketClient, msg: string) {.raises:[].} = +proc processMessage(client: LspSocketClient, msg: string) {.raises: [].} = try: - let serverReq = msg.parseJson() - if "method" in serverReq: + let serverReq = msg.parseJson() + if "method" in serverReq: let meth = serverReq["method"].jsonTo(string) debug "[Process Data Loop ]", meth = meth - if meth in client.notifications: + if meth in client.notifications: asyncSpawn client.notifications[meth](serverReq["params"]) - elif meth in client.routes: + elif meth in client.routes: asyncSpawn runRpc(client, client.routes[meth], serverReq) else: error "Method not implemented ", meth = meth @@ -75,7 +77,6 @@ proc processMessage(client: LspSocketClient, msg: string) {.raises:[].} = client.responses[id].complete(serverReq["result"]) else: error "Unknown msg", msg = msg - except CatchableError as exc: error "ProcessData Error ", msg = exc.msg @@ -92,10 +93,8 @@ proc processData(client: LspSocketClient) {.async: (raises: []).} = break # echo "----------------------------ProcessData----------------------" # echo value - # echo "----------------------------EndProcessData-------------------" + # echo "----------------------------EndProcessData-------------------" client.processMessage(value) - - except TransportError as exc: localException = newException(JsonRpcError, exc.msg) await client.transport.closeWait() @@ -103,23 +102,23 @@ proc processData(client: LspSocketClient) {.async: (raises: []).} = except CancelledError as exc: localException = newException(JsonRpcError, exc.msg) await client.transport.closeWait() - break + break if localException.isNil.not: - for _,fut in client.awaiting: + 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) + 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 + error "Error when reconnecting to server", msg = exc.msg break except CancelledError as exc: - error "Error when reconnecting to server", msg=exc.msg + error "Error when reconnecting to server", msg = exc.msg break proc connect*(client: LspSocketClient, address: string, port: Port) {.async.} = @@ -127,84 +126,87 @@ proc connect*(client: LspSocketClient, address: string, port: Port) {.async.} = client.transport = await connect(addresses[0]) client.address = addresses[0] client.loop = processData(client) - proc notify*(client: LspSocketClient, name: string, params: JsonNode) = - proc wrap(): Future[void] {.async.} = + proc wrap(): Future[void] {.async.} = discard await client.call(name, params) - asyncSpawn wrap() + asyncSpawn wrap() -proc register*(client: LspSocketClient, name: string, notRpc: NotificationRpc ) = +proc register*(client: LspSocketClient, name: string, notRpc: NotificationRpc) = client.notifications[name] = notRpc client.calls[name] = newSeq[JsonNode]() -proc register*(client: LspSocketClient, name: string, rpc: Rpc) = +proc register*(client: LspSocketClient, name: string, rpc: Rpc) = client.routes[name] = rpc - + #Calls -proc initialize*(client: LspSocketClient, initParams: InitializeParams): Future[InitializeResult] {.async.} = +proc initialize*( + client: LspSocketClient, initParams: InitializeParams +): Future[InitializeResult] {.async.} = client.call("initialize", %initParams).await.jsonTo(InitializeResult) - proc createDidOpenParams*(file: string): DidOpenTextDocumentParams = - return DidOpenTextDocumentParams %* { - "textDocument": { - "uri": fixtureUri(file), - "languageId": "nim", - "version": 0, - "text": readFile("tests" / file) - } - } - -proc positionParams*(uri: string, line, character: int): TextDocumentPositionParams = - return TextDocumentPositionParams %* { - "position": { - "line": line, - "character": character - }, + return + DidOpenTextDocumentParams %* { "textDocument": { - "uri": uri - } + "uri": fixtureUri(file), + "languageId": "nim", + "version": 0, + "text": readFile("tests" / file), + } } +proc positionParams*(uri: string, line, character: int): TextDocumentPositionParams = + return + TextDocumentPositionParams %* + {"position": {"line": line, "character": character}, "textDocument": {"uri": uri}} + #Helper to hook notifications so we can check against them in the tests -proc notificationHandle*(args: (LspSocketClient, string), params: JsonNode): Future[void] = +proc notificationHandle*( + args: (LspSocketClient, string), params: JsonNode +): Future[void] = try: let client = args[0] let name = args[1] - if name in [ - "textDocument/publishDiagnostics", - "$/progress" - ]: #Too much noise. They are split so we can toggle to debug the tests + if name in ["textDocument/publishDiagnostics", "$/progress"]: + #Too much noise. They are split so we can toggle to debug the tests debug "[NotificationHandled ] Called for ", name = name else: debug "[NotificationHandled ] Called for ", name = name, params = params - client.calls[name].add params - except CatchableError: discard + client.calls[name].add params + except CatchableError: + discard result = newFuture[void]("notificationHandle") -proc registerNotification*(client: LspSocketClient, names: varargs[string]) = +proc registerNotification*(client: LspSocketClient, names: varargs[string]) = for name in names: client.register(name, partial(notificationHandle, (client, name))) -proc waitForNotification*(client: LspSocketClient, name: string, predicate: proc(json: JsonNode): bool , accTime = 0): Future[bool] {.async.}= +proc waitForNotification*( + client: LspSocketClient, + name: string, + predicate: proc(json: JsonNode): bool {.gcsafe, raises: [CatchableError].}, + accTime = 0, +): Future[bool] {.async.} = let timeout = 10000 - if accTime > timeout: + if accTime > timeout: error "Coudlnt match predicate ", calls = client.calls[name] return false - try: - {.cast(gcsafe).}: - for call in client.calls[name]: - if predicate(call): - debug "[WaitForNotification Predicate Matches] ", name = name, call = call - return true - except Exception as ex: + try: + for call in client.calls[name]: + if predicate(call): + debug "[WaitForNotification Predicate Matches] ", name = name, call = call + return true + except CatchableError as ex: error "[WaitForNotification]", ex = ex.msg await sleepAsync(100) await waitForNotification(client, name, predicate, accTime + 100) - -proc waitForNotificationMessage*(client: LspSocketClient, msg: string): Future[bool] {.async.}= - return await waitForNotification(client, "window/showMessage", (json: JsonNode) => json["message"].to(string) == msg) +proc waitForNotificationMessage*( + client: LspSocketClient, msg: string +): Future[bool] {.async.} = + return await waitForNotification( + client, "window/showMessage", (json: JsonNode) => json["message"].to(string) == msg + ) diff --git a/utils.nim b/utils.nim index 4231142..0bc2514 100644 --- a/utils.nim +++ b/utils.nim @@ -11,9 +11,8 @@ type UriParseError* = object of Defect uri: string -const - NIM_SCRIPT_API_TEMPLATE* = staticRead("templates/nimscriptapi.nim") #We add this file to nimsuggest and `nim check` to support nimble files - +const NIM_SCRIPT_API_TEMPLATE* = staticRead("templates/nimscriptapi.nim") + #We add this file to nimsuggest and `nim check` to support nimble files proc writeStackTrace*(ex = getCurrentException()) = try: @@ -56,9 +55,7 @@ proc utf16Len*(utf8Str: string): int = result = 0 for rune in utf8Str.runes: case rune.int32 - of 0x0000 .. 0x007F, - 0x0080 .. 0x07FF, - 0x0800 .. 0xFFFF: + of 0x0000 .. 0x007F, 0x0080 .. 0x07FF, 0x0800 .. 0xFFFF: result += 1 of 0x10000 .. 0x10FFFF: result += 2 @@ -331,12 +328,12 @@ proc getNextFreePort*(): Port = func isWord*(str: string): bool = var str = str.toLower() for c in str: - if c.int notin {48..57, 97..122}: # Allow 0-9 and a-z + if c.int notin {48 .. 57, 97 .. 122}: # Allow 0-9 and a-z return false return true proc getNimScriptAPITemplatePath*(): string = - result = getCacheDir("nimlangserver") + result = getCacheDir("nimlangserver") createDir(result) result = result / "nimscriptapi.nim" @@ -345,10 +342,11 @@ proc getNimScriptAPITemplatePath*(): string = writeFile(result, NIM_SCRIPT_API_TEMPLATE) debug "NimScriptApiPath", path = result -proc shutdownChildProcess*(p: AsyncProcessRef): Future[void] {.async.} = +proc shutdownChildProcess*(p: AsyncProcessRef): Future[void] {.async.} = try: debug "Shutting down process with pid: ", pid = p.processID() - let exitCode = await p.terminateAndWaitForExit(2.seconds) # debug "Process terminated with exit code: ", exitCode + let exitCode = await p.terminateAndWaitForExit(2.seconds) + # debug "Process terminated with exit code: ", exitCode except CatchableError: try: let forcedExitCode = await p.killAndWaitForExit(3.seconds) @@ -360,54 +358,58 @@ proc shutdownChildProcess*(p: AsyncProcessRef): Future[void] {.async.} = macro getField*(obj: object, fld: string): untyped = result = newDotExpr(obj, newIdentNode(fld.strVal)) - proc readAllOutput*(stream: AsyncStreamReader): Future[string] {.async.} = result = "" while not stream.atEof: let data = await stream.read() result.add(string.fromBytes(data)) -proc readErrorOutputUntilExit*(process: AsyncProcessRef, duration: Duration): Future[tuple[output: string, code: int]] {.async.} = +proc readErrorOutputUntilExit*( + process: AsyncProcessRef, duration: Duration +): Future[tuple[output: string, code: int]] {.async.} = var output = "" var res = 0 while true: if not process.stderrStream.atEof: let data = await process.stderrStream.read() output.add(string.fromBytes(data)) - - let hasExited = try: - res = await process.waitForExit(duration) - true - except AsyncTimeoutError: - false - + + let hasExited = + try: + res = await process.waitForExit(duration) + true + except AsyncTimeoutError: + false + if hasExited: while not process.stderrStream.atEof: let data = await process.stderrStream.read() output.add(string.fromBytes(data)) return (output, res) - -proc readOutputUntilExit*(process: AsyncProcessRef, duration: Duration): Future[tuple[output: string, error: string, code: int]] {.async.} = +proc readOutputUntilExit*( + process: AsyncProcessRef, duration: Duration +): Future[tuple[output: string, error: string, code: int]] {.async.} = var output = "" var error = "" var res = 0 # debug "Starting read output until exit" - + while true: - let hasExited = try: - res = await process.waitForExit(duration) - debug "Process exit check", hasExited = true, res = res - true - except AsyncTimeoutError: - debug "Process still running" - false + let hasExited = + try: + res = await process.waitForExit(duration) + debug "Process exit check", hasExited = true, res = res + true + except AsyncTimeoutError: + debug "Process still running" + false # Quick non-blocking reads try: if not process.stdoutStream.atEof: # debug "Attempting stdout read" - let data = await process.stdoutStream.read() # + let data = await process.stdoutStream.read() # if data.len > 0: # debug "Got stdout data", len = data.len output.add(string.fromBytes(data)) @@ -417,14 +419,13 @@ proc readOutputUntilExit*(process: AsyncProcessRef, duration: Duration): Future[ try: if not process.stderrStream.atEof: # debug "Attempting stderr read" - let data = await process.stderrStream.read() + let data = await process.stderrStream.read() if data.len > 0: # debug "Got stderr data", len = data.len error.add(string.fromBytes(data)) except CatchableError as e: debug "Stderr read error", msg = e.msg - + if hasExited: # debug "Process has exited, final cleanup", output = output, error = error, code = res return (output, error, res) - \ No newline at end of file