diff --git a/ls.nim b/ls.nim index 2183a0f..9544642 100644 --- a/ls.nim +++ b/ls.nim @@ -1,9 +1,25 @@ -import macros, strformat, - chronos, chronos/threadsync, - os, sugar, hashes, osproc, - suggestapi, protocol/enums, protocol/types, with, tables, strutils, sets, - ./utils, chronicles, std/[re, json, streams, sequtils, setutils, times], uri, - json_serialization, json_rpc/[servers/socketserver] +import + macros, + strformat, + chronos, + chronos/threadsync, + os, + sugar, + hashes, + osproc, + suggestapi, + protocol/enums, + protocol/types, + with, + tables, + strutils, + sets, + ./utils, + chronicles, + std/[re, json, streams, sequtils, setutils, times], + uri, + json_serialization, + json_rpc/[servers/socketserver] proc getVersionFromNimble(): string = #We should static run nimble dump instead @@ -30,7 +46,6 @@ type projectFile*: string directory*: string - NlsInlayTypeHintsConfig* = ref object of RootObj enable*: Option[bool] @@ -65,6 +80,7 @@ type logNimsuggest*: Option[bool] inlayHints*: Option[NlsInlayHintsConfig] notificationVerbosity*: Option[NlsNotificationVerbosity] + formatOnSave*: Option[bool] NlsFileInfo* = ref object of RootObj projectFile*: Future[string] @@ -85,23 +101,27 @@ type ReadStdinContext* = object onStdReadSignal*: ThreadSignalPtr #used by the thread to notify it read from the std - onMainReadSignal*: ThreadSignalPtr #used by the main thread to notify it read the value from the signal + onMainReadSignal*: ThreadSignalPtr + #used by the main thread to notify it read the value from the signal value*: cstring - + PendingRequestState* = enum - prsOnGoing = "OnGoing", prsCancelled = "Cancelled", prsComplete = "Complete" + prsOnGoing = "OnGoing" + prsCancelled = "Cancelled" + prsComplete = "Complete" PendingRequest* = object id*: uint name*: string request*: Future[JsonString] - projectFile*: Option[string] + projectFile*: Option[string] startTime*: DateTime endTime*: DateTime state*: PendingRequestState - + LanguageServer* = ref object clientCapabilities*: ClientCapabilities + serverCapabilities*: ServerCapabilities extensionCapabilities*: set[LspExtensionCapability] initializeParams*: InitializeParams notify*: NotifyAction @@ -120,22 +140,26 @@ type storageDir*: string cmdLineClientProcessId*: Option[int] nimDumpCache*: Table[string, NimbleDumpInfo] #path to NimbleDumpInfo - entryPoints*: seq[string] - responseMap*: TableRef[string, Future[JsonNode]] #id to future. Represents the pending requests as result of calling ls.call - srv*: RpcSocketServer#Both modes uses it to store the routes. Only actually started in socket mode - pendingRequests*: Table[uint, PendingRequest] #id to future. Each request is added here so we can cancel them later in the cancelRequest. Only requests, not notifications + entryPoints*: seq[string] + responseMap*: TableRef[string, Future[JsonNode]] + #id to future. Represents the pending requests as result of calling ls.call + srv*: RpcSocketServer + #Both modes uses it to store the routes. Only actually started in socket mode + pendingRequests*: Table[uint, PendingRequest] + #id to future. Each request is added here so we can cancel them later in the cancelRequest. Only requests, not notifications case transportMode*: TransportMode of socket: - socketTransport*: StreamTransport + socketTransport*: StreamTransport of stdio: - outStream*: FileStream - stdinContext*: ptr ReadStdinContext - projectErrors*: seq[ProjectError] #List of errors (crashes) nimsuggest has had since the lsp session started + outStream*: FileStream + stdinContext*: ptr ReadStdinContext + projectErrors*: seq[ProjectError] + #List of errors (crashes) nimsuggest has had since the lsp session started Certainty* = enum - None, - Folder, - Cfg, + None + Folder + Cfg Nimble NimbleDumpInfo* = object @@ -145,13 +169,17 @@ type nimblePath*: Option[string] entryPoints*: seq[string] #when it's empty, means the nimble version doesnt dump it. - OnExitCallback* = proc (): Future[void] {.gcsafe, raises: [].} #To be called when the server is shutting down - NotifyAction* = proc (name: string, params: JsonNode) {. gcsafe, raises: [].} #Send a notification to the client - CallAction* = proc (name: string, params: JsonNode): Future[JsonNode] {. gcsafe, raises: [].} #Send a request to the client + OnExitCallback* = proc(): Future[void] {.gcsafe, raises: [].} + #To be called when the server is shutting down + NotifyAction* = proc(name: string, params: JsonNode) {.gcsafe, raises: [].} + #Send a notification to the client + CallAction* = + proc(name: string, params: JsonNode): Future[JsonNode] {.gcsafe, raises: [].} + #Send a request to the client macro `%*`*(t: untyped, inputStream: untyped): untyped = - result = newCall(bindSym("to", brOpen), - newCall(bindSym("%*", brOpen), inputStream), t) + result = + newCall(bindSym("to", brOpen), newCall(bindSym("%*", brOpen), inputStream), t) proc initLs*(params: CommandLineParams, storageDir: string): LanguageServer = LanguageServer( @@ -162,10 +190,12 @@ proc initLs*(params: CommandLineParams, storageDir: string): LanguageServer = responseMap: newTable[string, Future[JsonNode]](), storageDir: storageDir, cmdLineClientProcessId: params.clientProcessId, - extensionCapabilities: LspExtensionCapability.items.toSet + extensionCapabilities: LspExtensionCapability.items.toSet, ) -proc getNimbleEntryPoints*(dumpInfo: NimbleDumpInfo, nimbleProjectPath: string): seq[string] = +proc getNimbleEntryPoints*( + dumpInfo: NimbleDumpInfo, nimbleProjectPath: string +): seq[string] = if dumpInfo.entryPoints.len > 0: result = dumpInfo.entryPoints.mapIt(nimbleProjectPath / it) else: @@ -176,24 +206,28 @@ proc getNimbleEntryPoints*(dumpInfo: NimbleDumpInfo, nimbleProjectPath: string): func typeHintsEnabled*(cnf: NlsConfig): bool = result = true - if cnf.inlayHints.isSome and cnf.inlayHints.get.typeHints.isSome and cnf.inlayHints.get.typeHints.get.enable.isSome: + if cnf.inlayHints.isSome and cnf.inlayHints.get.typeHints.isSome and + cnf.inlayHints.get.typeHints.get.enable.isSome: result = cnf.inlayHints.get.typeHints.get.enable.get func exceptionHintsEnabled*(cnf: NlsConfig): bool = result = true - if cnf.inlayHints.isSome and cnf.inlayHints.get.exceptionHints.isSome and cnf.inlayHints.get.exceptionHints.get.enable.isSome: + if cnf.inlayHints.isSome and cnf.inlayHints.get.exceptionHints.isSome and + cnf.inlayHints.get.exceptionHints.get.enable.isSome: result = cnf.inlayHints.get.exceptionHints.get.enable.get func parameterHintsEnabled*(cnf: NlsConfig): bool = result = true - if cnf.inlayHints.isSome and cnf.inlayHints.get.parameterHints.isSome and cnf.inlayHints.get.parameterHints.get.enable.isSome: + if cnf.inlayHints.isSome and cnf.inlayHints.get.parameterHints.isSome and + cnf.inlayHints.get.parameterHints.get.enable.isSome: result = cnf.inlayHints.get.parameterHints.get.enable.get func inlayHintsEnabled*(cnf: NlsConfig): bool = typeHintsEnabled(cnf) or exceptionHintsEnabled(cnf) or parameterHintsEnabled(cnf) proc supportSignatureHelp*(cc: ClientCapabilities): bool = - if cc.isNil: return false + if cc.isNil: + return false let caps = cc.textDocument caps.isSome and caps.get.signatureHelp.isSome @@ -204,15 +238,16 @@ proc getNimbleDumpInfo*(ls: LanguageServer, nimbleFile: string): NimbleDumpInfo let info = execProcess("nimble dump " & nimbleFile) for line in info.splitLines: if line.startsWith("srcDir"): - result.srcDir = line[(1 + line.find '"')..^2] + result.srcDir = line[(1 + line.find '"') ..^ 2] if line.startsWith("name"): - result.name = line[(1 + line.find '"')..^2] + result.name = line[(1 + line.find '"') ..^ 2] if line.startsWith("nimDir"): - result.nimDir = some line[(1 + line.find '"')..^2] + result.nimDir = some line[(1 + line.find '"') ..^ 2] if line.startsWith("nimblePath"): - result.nimblePath = some line[(1 + line.find '"')..^2] + result.nimblePath = some line[(1 + line.find '"') ..^ 2] if line.startsWith("entryPoints"): - result.entryPoints = line[(1 + line.find '"')..^2].split(',').mapIt(it.strip(chars = {' ', '"'})) + result.entryPoints = + line[(1 + line.find '"') ..^ 2].split(',').mapIt(it.strip(chars = {' ', '"'})) var nimbleFile = nimbleFile if nimbleFile == "" and result.nimblePath.isSome: @@ -230,13 +265,18 @@ proc parseWorkspaceConfiguration*(conf: JsonNode): NlsConfig = discard try: let nlsConfig: seq[NlsConfig] = (%conf).to(seq[NlsConfig]) - result = if nlsConfig.len > 0 and nlsConfig[0] != nil: nlsConfig[0] else: NlsConfig() + result = + if nlsConfig.len > 0 and nlsConfig[0] != nil: + nlsConfig[0] + else: + NlsConfig() except CatchableError: debug "Failed to parse the configuration.", error = getCurrentExceptionMsg() result = NlsConfig() - -proc getWorkspaceConfiguration*(ls: LanguageServer): Future[NlsConfig] {.async: (raises:[]).} = +proc getWorkspaceConfiguration*( + ls: LanguageServer +): Future[NlsConfig] {.async: (raises: []).} = try: #this is the root of a lot a problems as there are multiple race conditions here. #since most request doenst really rely on the configuration, we can just go ahead and @@ -244,41 +284,45 @@ proc getWorkspaceConfiguration*(ls: LanguageServer): Future[NlsConfig] {.async: #TODO review and handle project specific confs when received instead of reliying in this func if ls.workspaceConfiguration.finished: return parseWorkspaceConfiguration(ls.workspaceConfiguration.read) - else: + else: return NlsConfig() except CatchableError as ex: writeStackTrace(ex) -proc showMessage*(ls: LanguageServer, message: string, typ: MessageType) {.raises: [].} = +proc showMessage*( + ls: LanguageServer, message: string, typ: MessageType +) {.raises: [].} = try: proc notify() = - ls.notify( - "window/showMessage", - %* { - "type": typ.int, - "message": message - }) - let verbosity = - ls - .getWorkspaceConfiguration - .waitFor - .notificationVerbosity.get(NlsNotificationVerbosity.nvInfo) + ls.notify("window/showMessage", %*{"type": typ.int, "message": message}) + + let verbosity = ls.getWorkspaceConfiguration.waitFor.notificationVerbosity.get( + NlsNotificationVerbosity.nvInfo + ) debug "ShowMessage ", message = message - case verbosity: + case verbosity of nvInfo: notify() of nvWarning: - if typ.int <= MessageType.Warning.int : + if typ.int <= MessageType.Warning.int: notify() of nvError: if typ == MessageType.Error: notify() - else: discard + else: + discard except CatchableError: discard -proc toPendingRequestStatus(pr: PendingRequest): PendingRequestStatus = - result.time = case pr.state: +proc applyEdit*( + ls: LanguageServer, params: ApplyWorkspaceEditParams +): Future[ApplyWorkspaceEditResponse] {.async.} = + let res = await ls.call("workspace/applyEdit", %params) + res.to(ApplyWorkspaceEditResponse) + +proc toPendingRequestStatus(pr: PendingRequest): PendingRequestStatus = + result.time = + case pr.state of prsOnGoing: $(now() - pr.startTime) else: @@ -300,13 +344,13 @@ proc getLspStatus*(ls: LanguageServer): NimLangServerStatus {.raises: [].} = capabilities: ns.capabilities.toSeq, version: ns.version, path: ns.nimsuggestPath, - port: ns.port + port: ns.port, ) for open in ns.openFiles: nsStatus.openFiles.add open result.nimsuggestInstances.add nsStatus except CatchableError: - discard + discard for openFile in ls.openFiles.keys: let openFilePath = openFile.uriToPath result.openFiles.add openFilePath @@ -314,41 +358,45 @@ proc getLspStatus*(ls: LanguageServer): NimLangServerStatus {.raises: [].} = result.pendingRequests = ls.pendingRequests.values.toSeq.map(toPendingRequestStatus) result.projectErrors = ls.projectErrors -proc sendStatusChanged*(ls: LanguageServer) {.raises: [].} = +proc sendStatusChanged*(ls: LanguageServer) {.raises: [].} = let status: NimLangServerStatus = ls.getLspStatus() - ls.notify("extension/statusUpdate", %* status) + ls.notify("extension/statusUpdate", %*status) -proc addProjectFileToPendingRequest*(ls: LanguageServer, id: uint, uri: string) {.async.}= +proc addProjectFileToPendingRequest*( + ls: LanguageServer, id: uint, uri: string +) {.async.} = if id in ls.pendingRequests: var projectFile = uri.uriToPath() if projectFile notin ls.projectFiles: if uri in ls.openFiles: - projectFile = await ls.openFiles[uri].projectFile + projectFile = await ls.openFiles[uri].projectFile - ls.pendingRequests[id].projectFile = some projectFile + ls.pendingRequests[id].projectFile = some projectFile ls.sendStatusChanged proc requiresDynamicRegistrationForDidChangeConfiguration(ls: LanguageServer): bool = ls.clientCapabilities.workspace.isSome and - ls.clientCapabilities.workspace.get.didChangeConfiguration.isSome and - ls.clientCapabilities.workspace.get.didChangeConfiguration.get.dynamicRegistration.get(false) + ls.clientCapabilities.workspace.get.didChangeConfiguration.isSome and + ls.clientCapabilities.workspace.get.didChangeConfiguration.get.dynamicRegistration.get( + false + ) proc supportsConfigurationRequest(ls: LanguageServer): bool = ls.clientCapabilities.workspace.isSome and - ls.clientCapabilities.workspace.get.configuration.get(false) + ls.clientCapabilities.workspace.get.configuration.get(false) proc usePullConfigurationModel*(ls: LanguageServer): bool = ls.requiresDynamicRegistrationForDidChangeConfiguration and - ls.supportsConfigurationRequest + ls.supportsConfigurationRequest proc inlayExceptionHintsConfigurationEquals*(a, b: NlsInlayHintsConfig): bool = if a.exceptionHints.isSome and b.exceptionHints.isSome: let ae = a.exceptionHints.get be = b.exceptionHints.get - result = (ae.enable == be.enable) and - (ae.hintStringLeft == be.hintStringLeft) and - (ae.hintStringRight == be.hintStringRight) + result = + (ae.enable == be.enable) and (ae.hintStringLeft == be.hintStringLeft) and + (ae.hintStringRight == be.hintStringRight) else: result = a.exceptionHints.isSome == b.exceptionHints.isSome @@ -359,7 +407,6 @@ proc inlayExceptionHintsConfigurationEquals*(a, b: NlsConfig): bool = result = a.inlayHints.isSome == b.inlayHints.isSome proc inlayHintsConfigurationEquals*(a, b: NlsConfig): bool = - proc inlayTypeHintsConfigurationEquals(a, b: NlsInlayHintsConfig): bool = if a.typeHints.isSome and b.typeHints.isSome: result = a.typeHints.get.enable == b.typeHints.get.enable @@ -367,8 +414,9 @@ proc inlayHintsConfigurationEquals*(a, b: NlsConfig): bool = result = a.typeHints.isSome == b.typeHints.isSome proc inlayHintsConfigurationEquals(a, b: NlsInlayHintsConfig): bool = - result = inlayTypeHintsConfigurationEquals(a, b) and - inlayExceptionHintsConfigurationEquals(a, b) + result = + inlayTypeHintsConfigurationEquals(a, b) and + inlayExceptionHintsConfigurationEquals(a, b) if a.inlayHints.isSome and b.inlayHints.isSome: result = inlayHintsConfigurationEquals(a.inlayHints.get, b.inlayHints.get) @@ -377,15 +425,19 @@ proc inlayHintsConfigurationEquals*(a, b: NlsConfig): bool = proc getNimVersion(nimDir: string): string = let cmd = - if nimDir == "": "nim --version" - else: nimDir / "nim --version" + if nimDir == "": + "nim --version" + else: + nimDir / "nim --version" let info = execProcess(cmd) const NimCompilerVersion = "Nim Compiler Version " for line in info.splitLines: if line.startsWith(NimCompilerVersion): return line -proc getNimSuggestPathAndVersion(ls: LanguageServer, conf: NlsConfig, workingDir: string): (string, string) = +proc getNimSuggestPathAndVersion( + ls: LanguageServer, conf: NlsConfig, workingDir: string +): (string, string) = #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 = ls.getNimbleDumpInfo("") let nimDir = nimbleDumpInfo.nimDir.get "" @@ -412,7 +464,8 @@ proc getProjectFileAutoGuess*(ls: LanguageServer, fileUri: string): string = var path = dir certainty = Certainty.None - up = 0 #Limit the times it goes up through the directories. Ideally nimble dump should do this job + up = 0 + #Limit the times it goes up through the directories. Ideally nimble dump should do this job while path.len > 0 and path != "/" and up < 2: let (dir, fname, ext) = path.splitFile() @@ -420,9 +473,10 @@ proc getProjectFileAutoGuess*(ls: LanguageServer, fileUri: string): string = if fileExists(path / current.addFileExt(".nim")) and certainty <= Folder: result = path / current.addFileExt(".nim") certainty = Folder - if fileExists(path / current.addFileExt(".nim")) and - (fileExists(path / current.addFileExt(".nim.cfg")) or - fileExists(path / current.addFileExt(".nims"))) and certainty <= Cfg: + if fileExists(path / current.addFileExt(".nim")) and ( + fileExists(path / current.addFileExt(".nim.cfg")) or + fileExists(path / current.addFileExt(".nims")) + ) and certainty <= Cfg: result = path / current.addFileExt(".nim") certainty = Cfg if certainty <= Nimble: @@ -431,13 +485,14 @@ proc getProjectFileAutoGuess*(ls: LanguageServer, fileUri: string): string = let name = dumpInfo.name let sourceDir = path / dumpInfo.srcDir let projectFile = sourceDir / (name & ".nim") - if sourceDir.len != 0 and name.len != 0 and - file.isRelTo(sourceDir) and fileExists(projectFile): + if sourceDir.len != 0 and name.len != 0 and file.isRelTo(sourceDir) and + fileExists(projectFile): debug "Found nimble project", projectFile = projectFile result = projectFile certainty = Nimble return - if path == dir: break + if path == dir: + break path = dir inc up @@ -460,32 +515,20 @@ proc getWorkingDir(ls: LanguageServer, path: string): Future[string] {.async.} = for m in mapping: if pathRelativeToRoot.isSome and m.projectFile == pathRelativeToRoot.get(): result = rootPath.string / m.directory - break; + break proc progressSupported(ls: LanguageServer): bool = - result = ls.initializeParams - .capabilities - .window - .get(ClientCapabilities_window()) - .workDoneProgress + result = ls.initializeParams.capabilities.window + .get(ClientCapabilities_window()).workDoneProgress .get(false) -proc progress*(ls: LanguageServer; token, kind: string, title = "") = +proc progress*(ls: LanguageServer, token, kind: string, title = "") = if ls.progressSupported: - ls.notify( - "$/progress", - %* { - "token": token, - "value": { - "kind": kind, - "title": title - } - }) + ls.notify("$/progress", %*{"token": token, "value": {"kind": kind, "title": title}}) proc workDoneProgressCreate*(ls: LanguageServer, token: string) = if ls.progressSupported: - discard ls.call("window/workDoneProgress/create", - %ProgressParams(token: token)) + discard ls.call("window/workDoneProgress/create", %ProgressParams(token: token)) proc cancelPendingFileChecks*(ls: LanguageServer, nimsuggest: Nimsuggest) = # stop all checks on file level if we are going to run checks on project @@ -508,16 +551,11 @@ proc uriToStash*(ls: LanguageServer, uri: string): string = "" proc range*(startLine, startCharacter, endLine, endCharacter: int): Range = - return Range %* { - "start": { - "line": startLine, - "character": startCharacter - }, - "end": { - "line": endLine, - "character": endCharacter - } - } + return + Range %* { + "start": {"line": startLine, "character": startCharacter}, + "end": {"line": endLine, "character": endCharacter}, + } proc toLabelRange*(suggest: Suggest): Range = with suggest: @@ -528,26 +566,28 @@ proc toDiagnostic(suggest: Suggest): Diagnostic = with suggest: let endColumn = column + doc.rfind('\'') - doc.find('\'') - 2 - node = %* { - "uri": pathToUri(filepath) , - "range": range(line - 1, column, line - 1, column + endColumn), - "severity": case forth: - of "Error": DiagnosticSeverity.Error.int - of "Hint": DiagnosticSeverity.Hint.int - of "Warning": DiagnosticSeverity.Warning.int - else: DiagnosticSeverity.Error.int, - "message": doc, - "source": "nim", - "code": "nimsuggest chk" - } + node = + %*{ + "uri": pathToUri(filepath), + "range": range(line - 1, column, line - 1, column + endColumn), + "severity": + case forth + of "Error": DiagnosticSeverity.Error.int + of "Hint": DiagnosticSeverity.Hint.int + of "Warning": DiagnosticSeverity.Warning.int + else: DiagnosticSeverity.Error.int + , + "message": doc, + "source": "nim", + "code": "nimsuggest chk", + } return node.to(Diagnostic) proc sendDiagnostics*(ls: LanguageServer, diagnostics: seq[Suggest], path: string) = debug "Sending diagnostics", count = diagnostics.len, path = path - let params = PublishDiagnosticsParams %* { - "uri": pathToUri(path), - "diagnostics": diagnostics.map(toDiagnostic) - } + let params = + PublishDiagnosticsParams %* + {"uri": pathToUri(path), "diagnostics": diagnostics.map(toDiagnostic)} ls.notify("textDocument/publishDiagnostics", %params) if diagnostics.len != 0: @@ -555,14 +595,17 @@ proc sendDiagnostics*(ls: LanguageServer, diagnostics: seq[Suggest], path: strin else: ls.filesWithDiags.excl path -proc tryGetNimsuggest*(ls: LanguageServer, uri: string): Future[Option[Nimsuggest]] {.async.} +proc tryGetNimsuggest*( + ls: LanguageServer, uri: string +): Future[Option[Nimsuggest]] {.async.} proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsafe.} = if not ls.getWorkspaceConfiguration.await().autoCheckProject.get(true): return debug "Running diagnostics", uri = uri let ns = ls.tryGetNimsuggest(uri).await - if ns.isNone: return + if ns.isNone: + return let nimsuggest = ns.get if nimsuggest.checkProjectInProgress: debug "Check project is already running", uri = uri @@ -575,11 +618,13 @@ 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 - proc getFilepath(s: Suggest): string = s.filepath + proc getFilepath(s: Suggest): string = + s.filepath + let - diagnostics = nimsuggest.chk(uriToPath(uri), ls.uriToStash(uri)) - .await() - .filter(sug => sug.filepath != "???") + diagnostics = nimsuggest.chk(uriToPath(uri), ls.uriToStash(uri)).await().filter( + sug => sug.filepath != "???" + ) filesWithDiags = diagnostics.map(s => s.filepath).toHashSet ls.progress(token, "end") @@ -592,61 +637,78 @@ proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsaf for path in ls.filesWithDiags: if not filesWithDiags.contains path: debug "Sending zero diags", path = path - let params = PublishDiagnosticsParams %* { - "uri": pathToUri(path), - "diagnostics": @[] - } + let params = + PublishDiagnosticsParams %* {"uri": pathToUri(path), "diagnostics": @[]} ls.notify("textDocument/publishDiagnostics", %params) ls.filesWithDiags = filesWithDiags nimsuggest.checkProjectInProgress = false if nimsuggest.needsCheckProject: nimsuggest.needsCheckProject = false - callSoon() do () {.gcsafe.}: + callSoon do() {.gcsafe.}: debug "Running delayed check project...", uri = uri traceAsyncErrors ls.checkProject(uri) -proc createOrRestartNimsuggest*(ls: LanguageServer, projectFile: string, uri = "") {.gcsafe, raises: [].} +proc createOrRestartNimsuggest*( + ls: LanguageServer, projectFile: string, uri = "" +) {.gcsafe, raises: [].} -proc onErrorCallback(args: (LanguageServer, string), project: Project) = - let +proc onErrorCallback(args: (LanguageServer, string), project: Project) = + let ls = args[0] uri = args[1] debug "NimSuggest needed to be restarted due to an error " let configuration = ls.getWorkspaceConfiguration().waitFor() warn "Server stopped.", projectFile = project.file try: - if configuration.autoRestart.get(true) and project.ns.completed and project.ns.read.successfullCall: + if configuration.autoRestart.get(true) and project.ns.completed and + project.ns.read.successfullCall: ls.createOrRestartNimsuggest(project.file, uri) else: - ls.showMessage(fmt "Server failed with {project.errorMessage}.", - MessageType.Error) + ls.showMessage( + fmt "Server failed with {project.errorMessage}.", MessageType.Error + ) except CatchableError as ex: error "An error has ocurred while handling nimsuggest err", msg = ex.msg writeStacktrace(ex) finally: - ls.projectErrors.add ProjectError(projectFile: project.file, errorMessage: project.errorMessage) + ls.projectErrors.add ProjectError( + projectFile: project.file, errorMessage: project.errorMessage + ) ls.sendStatusChanged() - -proc createOrRestartNimsuggest*(ls: LanguageServer, projectFile: string, uri = "") {.gcsafe, raises: [].} = +proc createOrRestartNimsuggest*( + ls: LanguageServer, projectFile: string, uri = "" +) {.gcsafe, raises: [].} = try: let configuration = ls.getWorkspaceConfiguration().waitFor() workingDir = ls.getWorkingDir(projectFile).waitFor() - (nimsuggestPath, version) = ls.getNimSuggestPathAndVersion(configuration, workingDir) + (nimsuggestPath, version) = + ls.getNimSuggestPathAndVersion(configuration, workingDir) timeout = configuration.timeout.get(REQUEST_TIMEOUT) - restartCallback = proc (ns: Nimsuggest) {.gcsafe, raises: [].} = - warn "Restarting the server due to requests being to slow", projectFile = projectFile - ls.showMessage(fmt "Restarting nimsuggest for file {projectFile} due to timeout.", - MessageType.Warning) + restartCallback = proc(ns: Nimsuggest) {.gcsafe, raises: [].} = + warn "Restarting the server due to requests being to slow", + projectFile = projectFile + ls.showMessage( + fmt "Restarting nimsuggest for file {projectFile} due to timeout.", + MessageType.Warning, + ) ls.createOrRestartNimsuggest(projectFile, uri) ls.sendStatusChanged() errorCallback = partial(onErrorCallback, (ls, uri)) #TODO instead of waiting here, this whole function should be async. - projectNext = waitFor createNimsuggest(projectFile, nimsuggestPath, version, - timeout, restartCallback, errorCallback, workingDir, configuration.logNimsuggest.get(false), - configuration.exceptionHintsEnabled) + projectNext = waitFor createNimsuggest( + projectFile, + nimsuggestPath, + version, + timeout, + restartCallback, + errorCallback, + workingDir, + configuration.logNimsuggest.get(false), + configuration.exceptionHintsEnabled, + ) token = fmt "Creating nimsuggest for {projectFile}" ls.workDoneProgressCreate(token) @@ -660,14 +722,15 @@ proc createOrRestartNimsuggest*(ls: LanguageServer, projectFile: string, uri = " ls.progress(token, "begin", fmt "Creating nimsuggest for {projectFile}") ls.projectFiles[projectFile] = projectNext - projectNext.ns.addCallback do (fut: Future[Nimsuggest]): + projectNext.ns.addCallback do(fut: Future[Nimsuggest]): if fut.read.project.failed: let msg = fut.read.project.errorMessage - ls.showMessage(fmt "Nimsuggest initialization for {projectFile} failed with: {msg}", - MessageType.Error) + ls.showMessage( + fmt "Nimsuggest initialization for {projectFile} failed with: {msg}", + MessageType.Error, + ) else: - ls.showMessage(fmt "Nimsuggest initialized for {projectFile}", - MessageType.Info) + ls.showMessage(fmt "Nimsuggest initialized for {projectFile}", MessageType.Info) traceAsyncErrors ls.checkProject(uri) fut.read().openFiles.incl uri ls.progress(token, "end") @@ -676,7 +739,7 @@ proc createOrRestartNimsuggest*(ls: LanguageServer, projectFile: string, uri = " discard proc getNimsuggestInner(ls: LanguageServer, uri: string): Future[Nimsuggest] {.async.} = - assert uri in ls.openFiles, "File not open" + assert uri in ls.openFiles, "File not open" let projectFile = await ls.openFiles[uri].projectFile if not ls.projectFiles.hasKey(projectFile): @@ -685,7 +748,9 @@ proc getNimsuggestInner(ls: LanguageServer, uri: string): Future[Nimsuggest] {.a ls.lastNimsuggest = ls.projectFiles[projectFile].ns return await ls.projectFiles[projectFile].ns -proc tryGetNimsuggest*(ls: LanguageServer, uri: string): Future[Option[Nimsuggest]] {.async.} = +proc tryGetNimsuggest*( + ls: LanguageServer, uri: string +): Future[Option[Nimsuggest]] {.async.} = if uri notin ls.openFiles: none(NimSuggest) else: @@ -699,18 +764,24 @@ proc restartAllNimsuggestInstances(ls: LanguageServer) = proc maybeRegisterCapabilityDidChangeConfiguration*(ls: LanguageServer) = if ls.requiresDynamicRegistrationForDidChangeConfiguration: let registrationParams = RegistrationParams( - registrations: some(@[Registration( - id: "a4606617-82c1-4e22-83db-0095fecb1093", - `method`: "workspace/didChangeConfiguration" - )]) + registrations: some( + @[ + Registration( + id: "a4606617-82c1-4e22-83db-0095fecb1093", + `method`: "workspace/didChangeConfiguration", + ) + ] + ) ) - ls.didChangeConfigurationRegistrationRequest = ls.call( - "client/registerCapability", - %registrationParams) - ls.didChangeConfigurationRegistrationRequest.addCallback() do (res: Future[JsonNode]): - debug "Got response for the didChangeConfiguration registration:", res = res.read() - -proc handleConfigurationChanges*(ls: LanguageServer, oldConfiguration, newConfiguration: NlsConfig) = + ls.didChangeConfigurationRegistrationRequest = + ls.call("client/registerCapability", %registrationParams) + ls.didChangeConfigurationRegistrationRequest.addCallback do(res: Future[JsonNode]): + debug "Got response for the didChangeConfiguration registration:", + res = res.read() + +proc handleConfigurationChanges*( + ls: LanguageServer, oldConfiguration, newConfiguration: NlsConfig +) = if ls.clientCapabilities.workspace.isSome and ls.clientCapabilities.workspace.get.inlayHint.isSome and ls.clientCapabilities.workspace.get.inlayHint.get.refreshSupport.get(false) and @@ -720,8 +791,7 @@ proc handleConfigurationChanges*(ls: LanguageServer, oldConfiguration, newConfig if not inlayExceptionHintsConfigurationEquals(oldConfiguration, newConfiguration): ls.restartAllNimsuggestInstances debug "Sending inlayHint refresh" - ls.inlayHintsRefreshRequest = ls.call("workspace/inlayHint/refresh", - newJNull()) + ls.inlayHintsRefreshRequest = ls.call("workspace/inlayHint/refresh", newJNull()) proc maybeRequestConfigurationFromClient*(ls: LanguageServer) = if ls.supportsConfigurationRequest: @@ -730,18 +800,18 @@ proc maybeRequestConfigurationFromClient*(ls: LanguageServer) = ls.prevWorkspaceConfiguration = ls.workspaceConfiguration - ls.workspaceConfiguration = - ls.call("workspace/configuration", - %configurationParams) - ls.workspaceConfiguration.addCallback() do (futConfiguration: Future[JsonNode]): + ls.workspaceConfiguration = ls.call("workspace/configuration", %configurationParams) + ls.workspaceConfiguration.addCallback do(futConfiguration: Future[JsonNode]): if futConfiguration.error.isNil: - debug "Received the following configuration", configuration = futConfiguration.read() - if not isNil(ls.prevWorkspaceConfiguration) and ls.prevWorkspaceConfiguration.finished: + debug "Received the following configuration", + configuration = futConfiguration.read() + if not isNil(ls.prevWorkspaceConfiguration) and + ls.prevWorkspaceConfiguration.finished: let - oldConfiguration = parseWorkspaceConfiguration(ls.prevWorkspaceConfiguration.read) + oldConfiguration = + parseWorkspaceConfiguration(ls.prevWorkspaceConfiguration.read) newConfiguration = parseWorkspaceConfiguration(futConfiguration.read) handleConfigurationChanges(ls, oldConfiguration, newConfiguration) - else: debug "Client does not support workspace/configuration" ls.workspaceConfiguration.complete(newJArray()) @@ -768,13 +838,21 @@ proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.asyn mappings = ls.getWorkspaceConfiguration.await().projectMapping.get(@[]) for mapping in mappings: - if pathRelativeToRoot.isSome and find(pathRelativeToRoot.get(), re(mapping.fileRegex), 0, pathRelativeToRoot.get().len) != -1: + if pathRelativeToRoot.isSome and + find( + pathRelativeToRoot.get(), + re(mapping.fileRegex), + 0, + pathRelativeToRoot.get().len, + ) != -1: result = string(rootPath) / mapping.projectFile if fileExists(result): - trace "getProjectFile?", project = result, uri = fileUri, matchedRegex = mapping.fileRegex + trace "getProjectFile?", + project = result, uri = fileUri, matchedRegex = mapping.fileRegex return result else: - trace "getProjectFile does not match", uri = fileUri, matchedRegex = mapping.fileRegex + trace "getProjectFile does not match", + uri = fileUri, matchedRegex = mapping.fileRegex result = ls.getProjectFileAutoGuess(fileUri) if result in ls.projectFiles: @@ -789,14 +867,17 @@ proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.asyn debug "getProjectFile ", project = result, fileUri = fileUri -proc warnIfUnknown*(ls: LanguageServer, ns: Nimsuggest, uri: string, projectFile: string): - Future[void] {.async, gcsafe.} = +proc warnIfUnknown*( + ls: LanguageServer, ns: Nimsuggest, uri: string, projectFile: string +): Future[void] {.async, gcsafe.} = let path = uri.uriToPath let isFileKnown = await ns.isKnown(path) if not isFileKnown and not ns.canHandleUnknown: - ls.showMessage(fmt """{path} is not compiled as part of project {projectFile}. + ls.showMessage( + fmt """{path} is not compiled as part of project {projectFile}. In orde to get the IDE features working you must either configure nim.projectMapping or import the module.""", - MessageType.Warning) + MessageType.Warning, + ) proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.async.} = debug "Checking", uri = uri @@ -807,28 +888,28 @@ proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.async.} = let path = uriToPath(uri) ns = await ls.tryGetNimsuggest(uri) - + if ns.isSome: - let diagnostics = ns.get() - .chkFile(path, ls.uriToStash(uri)) - .await() + let diagnostics = ns.get().chkFile(path, ls.uriToStash(uri)).await() ls.progress(token, "end") ls.sendDiagnostics(diagnostics, path) else: ls.progress(token, "end") - -proc removeCompletedPendingRequests(ls: LanguageServer, maxTimeAfterRequestWasCompleted = initDuration(seconds = 10)) = + +proc removeCompletedPendingRequests( + ls: LanguageServer, maxTimeAfterRequestWasCompleted = initDuration(seconds = 10) +) = var toRemove = newSeq[uint]() for id, pr in ls.pendingRequests: if pr.state != prsOnGoing: let passedTime = now() - pr.endTime if passedTime > maxTimeAfterRequestWasCompleted: toRemove.add id - + for id in toRemove: - ls.pendingRequests.del id - -proc tick*(ls: LanguageServer): Future[void] {.async.} = + ls.pendingRequests.del id + +proc tick*(ls: LanguageServer): Future[void] {.async.} = # debug "Ticking at ", now = now(), prs = ls.pendingRequests.len ls.removeCompletedPendingRequests() ls.sendStatusChanged diff --git a/protocol/types.nim b/protocol/types.nim index 00b5b87..b61a06f 100644 --- a/protocol/types.nim +++ b/protocol/types.nim @@ -9,8 +9,8 @@ type id*: OptionalNode Position* = ref object of RootObj - line*: int # uinteger - character*: int # uinteger + line*: int # uinteger + character*: int # uinteger Range* = ref object of RootObj start*: Position @@ -148,7 +148,8 @@ type resourceOperations*: OptionalSeq[ResourceOperationKind] failureHandling*: Option[FailureHandlingKind] normalizesLineEndings*: Option[bool] - changeAnnotationSupport*: Option[ChangeAnnotationSupportWorkspaceEditClientCapabilities] + changeAnnotationSupport*: + Option[ChangeAnnotationSupportWorkspaceEditClientCapabilities] DidChangeConfigurationClientCapabilities* = ref object of RootObj dynamicRegistration*: Option[bool] @@ -246,7 +247,8 @@ type tagSupport*: Option[CompletionClientCapabilities_completionItem_tagSupport] insertReplaceSupport*: Option[bool] resolveSupport*: Option[CompletionClientCapabilities_completionItem_resolveSupport] - insertTextModeSupport*: Option[CompletionClientCapabilities_completionItem_insertTextModeSupport] + insertTextModeSupport*: + Option[CompletionClientCapabilities_completionItem_insertTextModeSupport] labelDetailsSupport*: Option[bool] CompletionItemKind_int = int @@ -274,7 +276,8 @@ type SignatureHelpClientCapabilities_signatureInformation* = ref object of RootObj documentationFormat*: OptionalSeq[MarkupKind_str] - parameterInformation*: Option[SignatureHelpClientCapabilities_signatureInformation_parameterInformation] + parameterInformation*: + Option[SignatureHelpClientCapabilities_signatureInformation_parameterInformation] activeParameterSupport*: Option[bool] SignatureHelpClientCapabilities* = ref object of RootObj @@ -332,14 +335,16 @@ type valueSet*: seq[CodeActionKind_str] CodeActionClientCapabilities_codeActionLiteralSupport* = ref object of RootObj - codeActionKind*: CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind + codeActionKind*: + CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind CodeActionClientCapabilities_resolveSupport* = ref object of RootObj properties*: seq[string] CodeActionClientCapabilities* = ref object of RootObj dynamicRegistration*: Option[bool] - codeActionLiteralSupport*: Option[CodeActionClientCapabilities_codeActionLiteralSupport] + codeActionLiteralSupport*: + Option[CodeActionClientCapabilities_codeActionLiteralSupport] isPreferredSupport*: Option[bool] disabledSupport*: Option[bool] dataSupport*: Option[bool] @@ -391,7 +396,7 @@ type FoldingRangeClientCapabilities* = ref object of RootObj dynamicRegistration*: Option[bool] - rangeLimit*: Option[int] # uinteger + rangeLimit*: Option[int] # uinteger lineFoldingOnly*: Option[bool] foldingRangeKind*: Option[FoldingRangeClientCapabilities_foldingRangeKind] foldingRange*: Option[FoldingRangeClientCapabilities_foldingRange] @@ -406,8 +411,8 @@ type dynamicRegistration*: Option[bool] SemanticTokensClientCapabilities_requests* = ref object of RootObj - range*: OptionalNode # boolean | { } - full*: OptionalNode # boolean | { delta?: boolean; } + range*: OptionalNode # boolean | { } + full*: OptionalNode # boolean | { delta?: boolean; } # 'relative' TokenFormat_str = string @@ -561,7 +566,7 @@ type resolveProvider*: Option[bool] ExecuteCommandOptions* = ref object of RootObj - commands*: OptionalSeq[string] + commands*: OptionalSeq[string] SaveOptions* = ref object of RootObj includeText*: Option[bool] @@ -638,7 +643,8 @@ type declarationProvider*: Option[bool] definitionProvider*: Option[bool] typeDefinitionProvider*: Option[bool] - implementationProvider*: OptionalNode # bool or TextDocumentAndStaticRegistrationOptions + implementationProvider*: OptionalNode + # bool or TextDocumentAndStaticRegistrationOptions referencesProvider*: Option[bool] documentHighlightProvider*: Option[bool] documentSymbolProvider*: Option[bool] @@ -647,7 +653,8 @@ type codeLensProvider*: CodeLensOptions documentLinkProvider*: Option[DocumentLinkOptions] # colorProvider?: boolean | DocumentColorOptions | DocumentColorRegistrationOptions; - colorProvider*: OptionalNode # bool or ColorProviderOptions or TextDocumentAndStaticRegistrationOptions + colorProvider*: OptionalNode + # bool or ColorProviderOptions or TextDocumentAndStaticRegistrationOptions documentFormattingProvider*: Option[bool] documentRangeFormattingProvider*: Option[bool] documentOnTypeFormattingProvider*: DocumentOnTypeFormattingOptions @@ -661,7 +668,8 @@ type # monikerProvider?: boolean | MonikerOptions | MonikerRegistrationOptions; # typeHierarchyProvider?: boolean | TypeHierarchyOptions | TypeHierarchyRegistrationOptions; # inlineValueProvider?: boolean | InlineValueOptions | InlineValueRegistrationOptions; - inlayHintProvider*: Option[InlayHintOptions] # boolean | InlayHintOptions | InlayHintRegistrationOptions; + inlayHintProvider*: Option[InlayHintOptions] + # boolean | InlayHintOptions | InlayHintRegistrationOptions; # diagnosticProvider?: DiagnosticOptions | DiagnosticRegistrationOptions; # workspaceSymbolProvider?: boolean | WorkspaceSymbolOptions; workspace*: Option[ServerCapabilities_workspace] @@ -817,7 +825,8 @@ type value*: string Hover* = ref object of RootObj - contents*: OptionalNode # string or MarkedStringOption or [string] or [MarkedStringOption] or MarkupContent + contents*: OptionalNode + # string or MarkedStringOption or [string] or [MarkedStringOption] or MarkupContent range*: Option[Range] HoverParams* = ref object of TextDocumentPositionParams @@ -833,8 +842,7 @@ type parameters*: seq[ParameterInformation] ParameterInformation* = ref object of RootObj - label*: string - # documentation*: Option[string] + label*: string # documentation*: Option[string] SignatureHelpRegistrationOptions* = ref object of TextDocumentRegistrationOptions triggerCharacters*: OptionalSeq[string] @@ -956,7 +964,7 @@ type range*: Range content*: string - InlayHintParams* = ref object of RootObj # TODO: extends WorkDoneProgressParams + InlayHintParams* = ref object of RootObj # TODO: extends WorkDoneProgressParams textDocument*: TextDocumentIdentifier range*: Range @@ -964,23 +972,22 @@ type InlayHint* = ref object of RootObj position*: Position - label*: string # string | InlayHintLabelPart[] + label*: string # string | InlayHintLabelPart[] kind*: Option[InlayHintKind_int] textEdits*: OptionalSeq[TextEdit] - tooltip*: Option[string] # string | MarkupContent + tooltip*: Option[string] # string | MarkupContent paddingLeft*: Option[bool] - paddingRight*: Option[bool] - #data*: OptionalNode - - NimSuggestCapability* = enum - nsCon = "con", + paddingRight*: Option[bool] #data*: OptionalNode + + NimSuggestCapability* = enum + nsCon = "con" nsExceptionInlayHints = "exceptionInlayHints" nsUnknownFile = "unknownFile" - + PendingRequestStatus* = object name*: string projectFile*: string - time*: string + time*: string state*: string NimSuggestStatus* = object @@ -991,14 +998,14 @@ type port*: int openFiles*: seq[string] unknownFiles*: seq[string] - - LspExtensionCapability* = enum #List of extensions this server support. Useful for clients + + LspExtensionCapability* = enum + #List of extensions this server support. Useful for clients excRestartSuggest = "RestartSuggest" ProjectError* = object - projectFile*: string - errorMessage*: string - #last known cmd? last know request? + projectFile*: string + errorMessage*: string #last known cmd? last know request? NimLangServerStatus* = object version*: string @@ -1009,13 +1016,15 @@ type projectErrors*: seq[ProjectError] NimLangServerStatusParams* = object - + SuggestAction* = enum - saNone = "none", saRestart = "restart", saRestartAll = "restartAll" - + saNone = "none" + saRestart = "restart" + saRestartAll = "restartAll" + SuggestParams* = object action*: SuggestAction projectFile*: string #Absolute path to file - + SuggestResult* = object - actionPerformed*: SuggestAction \ No newline at end of file + actionPerformed*: SuggestAction diff --git a/routes.nim b/routes.nim index 564defc..6947dc7 100644 --- a/routes.nim +++ b/routes.nim @@ -1,36 +1,54 @@ -import macros, strformat, chronos, chronos/asyncproc, - json_rpc/server, os, sugar, sequtils, - suggestapi, protocol/enums, protocol/types, with, tables, strutils, - ./utils, chronicles, - asyncprocmonitor, json_serialization, - std/[strscans, times, json, parseutils], ls - -proc getNphPath(): Option[string] = +import + macros, + strformat, + chronos, + chronos/asyncproc, + json_rpc/server, + os, + sugar, + sequtils, + suggestapi, + protocol/enums, + protocol/types, + with, + tables, + strutils, + ./utils, + chronicles, + asyncprocmonitor, + json_serialization, + std/[strscans, times, json, parseutils], + ls, + stew/[byteutils] + +proc getNphPath(): Option[string] = let path = findExe "nph" - if path == "": none(string) - else: some path + if path == "": + none(string) + else: + some path #routes -proc initialize*(p: tuple[ls: LanguageServer, onExit: OnExitCallback], params: InitializeParams): - Future[InitializeResult] {.async.} = - - proc onClientProcessExitAsync(): Future[void] {.async.} = - debug "onClientProcessExitAsync" - await p.ls.stopNimsuggestProcesses - await p.onExit() - - proc onClientProcessExit() {.closure, gcsafe.} = - try: - debug "onClientProcessExit" - waitFor onClientProcessExitAsync() - except Exception: - error "Error in onClientProcessExit ", msg = getCurrentExceptionMsg() +proc initialize*( + p: tuple[ls: LanguageServer, onExit: OnExitCallback], params: InitializeParams +): Future[InitializeResult] {.async.} = + proc onClientProcessExitAsync(): Future[void] {.async.} = + debug "onClientProcessExitAsync" + await p.ls.stopNimsuggestProcesses + await p.onExit() + + proc onClientProcessExit() {.closure, gcsafe.} = + try: + debug "onClientProcessExit" + waitFor onClientProcessExitAsync() + except Exception: + error "Error in onClientProcessExit ", msg = getCurrentExceptionMsg() debug "Initialize received..." if params.processId.isSome: let pid = params.processId.get if pid.kind == JInt: - debug "Registering monitor for process ", pid=pid.num + debug "Registering monitor for process ", pid = pid.num var pidInt = int(pid.num) if p.ls.cmdLineClientProcessId.isSome: if p.ls.cmdLineClientProcessId.get == pidInt: @@ -45,39 +63,39 @@ proc initialize*(p: tuple[ls: LanguageServer, onExit: OnExitCallback], params: I p.ls.clientCapabilities = params.capabilities result = InitializeResult( capabilities: ServerCapabilities( - textDocumentSync: some(%TextDocumentSyncOptions( - openClose: some(true), - change: some(TextDocumentSyncKind.Full.int), - willSave: some(false), - willSaveWaitUntil: some(false), - save: some(SaveOptions(includeText: some(true)))) + textDocumentSync: some( + %TextDocumentSyncOptions( + openClose: some(true), + change: some(TextDocumentSyncKind.Full.int), + willSave: some(false), + willSaveWaitUntil: some(false), + save: some(SaveOptions(includeText: some(true))), + ) ), hoverProvider: some(true), - workspace: some(ServerCapabilities_workspace( - workspaceFolders: some(WorkspaceFoldersServerCapabilities()) - )), - completionProvider: CompletionOptions( - triggerCharacters: some(@["."]), - resolveProvider: some(false) - ), - signatureHelpProvider: SignatureHelpOptions( - triggerCharacters: some(@["(", ","]) + workspace: some( + ServerCapabilities_workspace( + workspaceFolders: some(WorkspaceFoldersServerCapabilities()) + ) ), + completionProvider: + CompletionOptions(triggerCharacters: some(@["."]), resolveProvider: some(false)), + signatureHelpProvider: SignatureHelpOptions(triggerCharacters: some(@["(", ","])), definitionProvider: some(true), declarationProvider: some(true), typeDefinitionProvider: some(true), referencesProvider: some(true), documentHighlightProvider: some(true), workspaceSymbolProvider: some(true), - executeCommandProvider: some(ExecuteCommandOptions( - commands: some(@[RESTART_COMMAND, RECOMPILE_COMMAND, CHECK_PROJECT_COMMAND]) - )), - inlayHintProvider: some(InlayHintOptions( - resolveProvider: some(false) - )), + executeCommandProvider: some( + ExecuteCommandOptions( + commands: some(@[RESTART_COMMAND, RECOMPILE_COMMAND, CHECK_PROJECT_COMMAND]) + ) + ), + inlayHintProvider: some(InlayHintOptions(resolveProvider: some(false))), documentSymbolProvider: some(true), codeActionProvider: some(true), - documentFormattingProvider: some(getNphPath().isSome) + documentFormattingProvider: some(getNphPath().isSome), ) ) # Support rename by default, but check if we can also support prepare @@ -87,52 +105,53 @@ proc initialize*(p: tuple[ls: LanguageServer, onExit: OnExitCallback], params: I # Check if the client support prepareRename #TODO do the test on the action if docCaps.rename.isSome and docCaps.rename.get().prepareSupport.get(false): - result.capabilities.renameProvider = %* { - "prepareProvider": true - } + result.capabilities.renameProvider = %*{"prepareProvider": true} debug "Initialize completed. Trying to start nimsuggest instances" -# nce we refactor the project to chronos, we may move this code into init. Right now it hangs for some odd reason + let ls = p.ls + ls.serverCapabilities = result.capabilities let rootPath = ls.initializeParams.getRootPath if rootPath != "": let nimbleFiles = walkFiles(rootPath / "*.nimble").toSeq if nimbleFiles.len > 0: let nimbleFile = nimbleFiles[0] let nimbleDumpInfo = ls.getNimbleDumpInfo(nimbleFile) - ls.entryPoints = nimbleDumpInfo.getNimbleEntryPoints(ls.initializeParams.getRootPath) + ls.entryPoints = + nimbleDumpInfo.getNimbleEntryPoints(ls.initializeParams.getRootPath) # ls.showMessage(fmt "Found entry point {ls.entryPoints}?", MessageType.Info) for entryPoint in ls.entryPoints: debug "Starting nimsuggest for entry point ", entry = entryPoint if entryPoint notin ls.projectFiles: ls.createOrRestartNimsuggest(entryPoint) - #If we are in a nimble project here, we try to start the entry points - proc toCompletionItem(suggest: Suggest): CompletionItem = with suggest: - return CompletionItem %* { - "label": qualifiedPath[^1].strip(chars = {'`'}), - "kind": nimSymToLSPKind(suggest).int, - "documentation": doc, - "detail": nimSymDetails(suggest), - } + return + CompletionItem %* { + "label": qualifiedPath[^1].strip(chars = {'`'}), + "kind": nimSymToLSPKind(suggest).int, + "documentation": doc, + "detail": nimSymDetails(suggest), + } -proc completion*(ls: LanguageServer, params: CompletionParams, id: int): - Future[seq[CompletionItem]] {.async.} = +proc completion*( + ls: LanguageServer, params: CompletionParams, id: int +): Future[seq[CompletionItem]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) let nimsuggest = await ls.tryGetNimsuggest(uri) if nimsuggest.isNone(): return @[] - let - completions = await nimsuggest.get - .sug(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + let completions = await nimsuggest.get.sug( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) result = completions.map(toCompletionItem) - if ls.clientCapabilities.supportSignatureHelp() and nsCon in nimSuggest.get.capabilities: + if ls.clientCapabilities.supportSignatureHelp() and + nsCon in nimSuggest.get.capabilities: #show only unique overloads if we support signatureHelp var unique = initTable[string, CompletionItem]() for completion in result: @@ -141,161 +160,191 @@ proc completion*(ls: LanguageServer, params: CompletionParams, id: int): result = unique.values.toSeq proc toLocation*(suggest: Suggest): Location = - return Location %* { - "uri": pathToUri(suggest.filepath), - "range": toLabelRange(suggest) - } + return + Location %* {"uri": pathToUri(suggest.filepath), "range": toLabelRange(suggest)} -proc definition*(ls: LanguageServer, params: TextDocumentPositionParams, id: int): - Future[seq[Location]] {.async.} = +proc definition*( + ls: LanguageServer, params: TextDocumentPositionParams, id: int +): Future[seq[Location]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) let ns = await ls.tryGetNimsuggest(uri) - if ns.isNone: return @[] - result = - ns.get - .def(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + if ns.isNone: + return @[] + result = ns.get + .def( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) .await() .map(toLocation) -proc declaration*(ls: LanguageServer, params: TextDocumentPositionParams, id: int): - Future[seq[Location]] {.async.} = +proc declaration*( + ls: LanguageServer, params: TextDocumentPositionParams, id: int +): Future[seq[Location]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) let ns = await ls.tryGetNimsuggest(uri) - if ns.isNone: return @[] + if ns.isNone: + return @[] result = ns.get - .declaration(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + .declaration( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) .await() .map(toLocation) -proc expandAll*(ls: LanguageServer, params: TextDocumentPositionParams): - Future[ExpandResult] {.async.} = +proc expandAll*( + ls: LanguageServer, params: TextDocumentPositionParams +): Future[ExpandResult] {.async.} = with (params.position, params.textDocument): let ns = await ls.tryGetNimsuggest(uri) - if ns.isNone: return ExpandResult() #TODO make it optional - + if ns.isNone: + return ExpandResult() #TODO make it optional + let expand = ns.get - .expand(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + .expand( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) .await() proc createRangeFromSuggest(suggest: Suggest): Range = - result = range(suggest.line - 1, - 0, - suggest.endLine - 1, - suggest.endCol) + result = range(suggest.line - 1, 0, suggest.endLine - 1, suggest.endCol) proc fixIdentation(s: string, indent: int): string = - result = s.split("\n") - .mapIt(if (it != ""): - repeat(" ", indent) & it - else: - it) + result = s + .split("\n") + .mapIt( + if (it != ""): + repeat(" ", indent) & it + else: + it + ) .join("\n") -proc expand*(ls: LanguageServer, params: ExpandTextDocumentPositionParams): - Future[ExpandResult] {.async} = +proc expand*( + ls: LanguageServer, params: ExpandTextDocumentPositionParams +): Future[ExpandResult] {.async.} = with (params, params.position, params.textDocument): let lvl = level.get(-1) - tag = if lvl == -1: "all" else: $lvl + tag = + if lvl == -1: + "all" + else: + $lvl ns = await ls.tryGetNimsuggest(uri) - if ns.isNone: return ExpandResult() - let - expand = ns.get - .expand(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character), - fmt " {tag}") - .await() + if ns.isNone: + return ExpandResult() + let expand = ns.get + .expand( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + fmt " {tag}", + ) + .await() if expand.len != 0: - result = ExpandResult(content: expand[0].doc.fixIdentation(character), - range: expand[0].createRangeFromSuggest()) + result = ExpandResult( + content: expand[0].doc.fixIdentation(character), + range: expand[0].createRangeFromSuggest(), + ) -proc status*(ls: LanguageServer, params: NimLangServerStatusParams): Future[NimLangServerStatus] {.async.} = +proc status*( + ls: LanguageServer, params: NimLangServerStatusParams +): Future[NimLangServerStatus] {.async.} = debug "Received status request" ls.getLspStatus() -proc extensionCapabilities*(ls: LanguageServer, _: JsonNode): Future[seq[string]] {.async.} = +proc extensionCapabilities*( + ls: LanguageServer, _: JsonNode +): Future[seq[string]] {.async.} = ls.extensionCapabilities.toSeq.mapIt($it) -proc extensionSuggest*(ls: LanguageServer, params: SuggestParams): Future[SuggestResult] {.async.} = - debug "[Extension Suggest]", params = params - var projectFile = params.projectFile - if projectFile != "" and projectFile notin ls.projectFiles: +proc extensionSuggest*( + ls: LanguageServer, params: SuggestParams +): Future[SuggestResult] {.async.} = + debug "[Extension Suggest]", params = params + var projectFile = params.projectFile + if projectFile != "" and projectFile notin ls.projectFiles: #test if just a regular file - let uri = projectFile.pathToUri + let uri = projectFile.pathToUri if uri in ls.openFiles: let openFile = ls.openFiles[uri] projectFile = await openFile.projectFile - debug "[ExtensionSuggest] Found project file for ", file = params.projectFile, project = projectFile + debug "[ExtensionSuggest] Found project file for ", + file = params.projectFile, project = projectFile else: error "Project file must exists ", params = params return SuggestResult() - template restart(ls: LanguageServer, project: Project) = + template restart(ls: LanguageServer, project: Project) = ls.showMessage(fmt "Restarting nimsuggest {projectFile}", MessageType.Info) - project.errorCallback = nil + project.errorCallback = nil project.stop() ls.createOrRestartNimsuggest(projectFile, projectFile.pathToUri) ls.sendStatusChanged() - - case params.action: - of saRestart: + + case params.action + of saRestart: let project = ls.projectFiles[projectFile] ls.restart(project) SuggestResult(actionPerformed: saRestart) of saRestartAll: let projectFiles = ls.projectFiles.keys.toSeq() - for projectFile in projectFiles: + for projectFile in projectFiles: let project = ls.projectFiles[projectFile] ls.restart(project) SuggestResult(actionPerformed: saRestartAll) of saNone: error "An action must be specified", params = params - SuggestResult() + SuggestResult() -proc typeDefinition*(ls: LanguageServer, params: TextDocumentPositionParams, id: int): - Future[seq[Location]] {.async.} = +proc typeDefinition*( + ls: LanguageServer, params: TextDocumentPositionParams, id: int +): Future[seq[Location]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) let ns = await ls.tryGetNimSuggest(uri) - if ns.isNone: return @[] + if ns.isNone: + return @[] result = ns.get - .`type`(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + .`type`( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) .await() .map(toLocation) proc toSymbolInformation*(suggest: Suggest): SymbolInformation = with suggest: - return SymbolInformation %* { - "location": toLocation(suggest), - "kind": nimSymToLSPSymbolKind(suggest.symKind).int, - "name": suggest.name - } + return + SymbolInformation %* { + "location": toLocation(suggest), + "kind": nimSymToLSPSymbolKind(suggest.symKind).int, + "name": suggest.name, + } -proc documentSymbols*(ls: LanguageServer, params: DocumentSymbolParams, id: int): - Future[seq[SymbolInformation]] {.async.} = +proc documentSymbols*( + ls: LanguageServer, params: DocumentSymbolParams, id: int +): Future[seq[SymbolInformation]] {.async.} = let uri = params.textDocument.uri asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) let ns = await ls.tryGetNimsuggest(uri) if ns.isSome: - ns.get() - .outline(uriToPath(uri), ls.uriToStash(uri)) - .await() - .map(toSymbolInformation) + ns.get().outline(uriToPath(uri), ls.uriToStash(uri)).await().map( + toSymbolInformation + ) else: @[] @@ -314,17 +363,17 @@ proc scheduleFileCheck(ls: LanguageServer, uri: string) {.gcsafe, raises: [].} = var cancelFuture = newFuture[void]() fileData.cancelFileCheck = cancelFuture - sleepAsync(FILE_CHECK_DELAY).addCallback() do (): + sleepAsync(FILE_CHECK_DELAY).addCallback do(): if not cancelFuture.finished: fileData.checkInProgress = true - ls.checkFile(uri).addCallback() do() {.gcsafe, raises:[].}: + ls.checkFile(uri).addCallback do() {.gcsafe, raises: [].}: try: ls.openFiles[uri].checkInProgress = false if fileData.needsChecking: fileData.needsChecking = false ls.scheduleFileCheck(uri) except KeyError: - discard + discard # except Exception: # discard @@ -333,68 +382,61 @@ proc toMarkedStrings(suggest: Suggest): seq[MarkedStringOption] = if suggest.forth != "": label &= ": " & suggest.forth - result = @[ - MarkedStringOption %* { - "language": "nim", - "value": label - } - ] + result = @[MarkedStringOption %* {"language": "nim", "value": label}] if suggest.doc != "": - result.add MarkedStringOption %* { - "language": "markdown", - "value": suggest.doc - } + result.add MarkedStringOption %* {"language": "markdown", "value": suggest.doc} -proc hover*(ls: LanguageServer, params: HoverParams, id: int): - Future[Option[Hover]] {.async.} = +proc hover*( + ls: LanguageServer, params: HoverParams, id: int +): Future[Option[Hover]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) - let - nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone: + let nimsuggest = await ls.tryGetNimsuggest(uri) + if nimsuggest.isNone: return none(Hover) - let - suggestions = await nimsuggest.get() - .def(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) + let suggestions = await nimsuggest.get().def( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) if suggestions.len == 0: - return none[Hover](); + return none[Hover]() else: return some(Hover(contents: some(%toMarkedStrings(suggestions[0])))) -proc references*(ls: LanguageServer, params: ReferenceParams): - Future[seq[Location]] {.async.} = +proc references*( + ls: LanguageServer, params: ReferenceParams +): Future[seq[Location]] {.async.} = with (params.position, params.textDocument, params.context): - let - nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone: return @[] - let - refs = await nimsuggest.get - .use(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) - result = refs - .filter(suggest => suggest.section != ideDef or includeDeclaration) - .map(toLocation); - -proc prepareRename*(ls: LanguageServer, params: PrepareRenameParams, - id: int): Future[JsonNode] {.async.} = + let nimsuggest = await ls.tryGetNimsuggest(uri) + if nimsuggest.isNone: + return @[] + let refs = await nimsuggest.get.use( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) + result = refs.filter(suggest => suggest.section != ideDef or includeDeclaration).map( + toLocation + ) + +proc prepareRename*( + ls: LanguageServer, params: PrepareRenameParams, id: int +): Future[JsonNode] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) - let - nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone: return newJNull() - let - def = await nimsuggest.get.def( - uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character) - ) + let nimsuggest = await ls.tryGetNimsuggest(uri) + if nimsuggest.isNone: + return newJNull() + let def = await nimsuggest.get.def( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) if def.len == 0: return newJNull() # Check if the symbol belongs to the project @@ -404,13 +446,17 @@ proc prepareRename*(ls: LanguageServer, params: PrepareRenameParams, return newJNull() -proc rename*(ls: LanguageServer, params: RenameParams, id: int): Future[WorkspaceEdit] {.async.} = +proc rename*( + ls: LanguageServer, params: RenameParams, id: int +): Future[WorkspaceEdit] {.async.} = # We reuse the references command as to not duplicate it - let references = await ls.references(ReferenceParams( - context: ReferenceContext(includeDeclaration: true), - textDocument: params.textDocument, - position: params.position - )) + let references = await ls.references( + ReferenceParams( + context: ReferenceContext(includeDeclaration: true), + textDocument: params.textDocument, + position: params.position, + ) + ) # Build up list of edits that the client needs to perform for each file let projectDir = ls.initializeParams.getRootPath var edits = newJObject() @@ -433,46 +479,47 @@ proc convertInlayHintKind(kind: SuggestInlayHintKind): InlayHintKind_int = # LSP doesn't have an exception inlay hint type, so we pretend (i.e. lie) that it is a type hint. result = 1 -proc toInlayHint(suggest: SuggestInlayHint; configuration: NlsConfig): InlayHint = +proc toInlayHint(suggest: SuggestInlayHint, configuration: NlsConfig): InlayHint = let hint_line = suggest.line - 1 # TODO: how to convert column? var hint_col = suggest.column result = InlayHint( - position: Position( - line: hint_line, - character: hint_col - ), + position: Position(line: hint_line, character: hint_col), label: suggest.label, kind: some(convertInlayHintKind(suggest.kind)), paddingLeft: some(suggest.paddingLeft), - paddingRight: some(suggest.paddingRight) + paddingRight: some(suggest.paddingRight), ) - if suggest.kind == sihkException and suggest.label == "try " and configuration.inlayHints.isSome and configuration.inlayHints.get.exceptionHints.isSome and configuration.inlayHints.get.exceptionHints.get.hintStringLeft.isSome: + if suggest.kind == sihkException and suggest.label == "try " and + configuration.inlayHints.isSome and + configuration.inlayHints.get.exceptionHints.isSome and + configuration.inlayHints.get.exceptionHints.get.hintStringLeft.isSome: result.label = configuration.inlayHints.get.exceptionHints.get.hintStringLeft.get - if suggest.kind == sihkException and suggest.label == "!" and configuration.inlayHints.isSome and configuration.inlayHints.get.exceptionHints.isSome and configuration.inlayHints.get.exceptionHints.get.hintStringRight.isSome: + if suggest.kind == sihkException and suggest.label == "!" and + configuration.inlayHints.isSome and + configuration.inlayHints.get.exceptionHints.isSome and + configuration.inlayHints.get.exceptionHints.get.hintStringRight.isSome: result.label = configuration.inlayHints.get.exceptionHints.get.hintStringRight.get if suggest.tooltip != "": result.tooltip = some(suggest.tooltip) else: result.tooltip = some("") if suggest.allowInsert: - result.textEdits = some(@[ - TextEdit( - newText: suggest.label, - `range`: Range( - start: Position( - line: hint_line, - character: hint_col + result.textEdits = some( + @[ + TextEdit( + newText: suggest.label, + `range`: Range( + start: Position(line: hint_line, character: hint_col), + `end`: Position(line: hint_line, character: hint_col), ), - `end`: Position( - line: hint_line, - character: hint_col - ) ) - ) - ]) + ] + ) -proc inlayHint*(ls: LanguageServer, params: InlayHintParams, id: int): Future[seq[InlayHint]] {.async.} = +proc inlayHint*( + ls: LanguageServer, params: InlayHintParams, id: int +): Future[seq[InlayHint]] {.async.} = debug "inlayHint received..." with (params.range, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) @@ -480,57 +527,72 @@ proc inlayHint*(ls: LanguageServer, params: InlayHintParams, id: int): Future[se configuration = ls.getWorkspaceConfiguration.await() nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone or nimsuggest.get.protocolVersion < 4 or not configuration.inlayHintsEnabled: + if nimsuggest.isNone or nimsuggest.get.protocolVersion < 4 or + not configuration.inlayHintsEnabled: return @[] - let - suggestions = await nimsuggest.get - .inlayHints(uriToPath(uri), - ls.uriToStash(uri), - start.line + 1, - ls.getCharacter(uri, start.line, start.character), - `end`.line + 1, - ls.getCharacter(uri, `end`.line, `end`.character), - " +exceptionHints +parameterHints") + let suggestions = await nimsuggest.get.inlayHints( + uriToPath(uri), + ls.uriToStash(uri), + start.line + 1, + ls.getCharacter(uri, start.line, start.character), + `end`.line + 1, + ls.getCharacter(uri, `end`.line, `end`.character), + " +exceptionHints +parameterHints", + ) result = suggestions - .filter(x => ((x.inlayHintInfo.kind == sihkType) and configuration.typeHintsEnabled) or - ((x.inlayHintInfo.kind == sihkException) and configuration.exceptionHintsEnabled) or - ((x.inlayHintInfo.kind == sihkParameter) and configuration.parameterHintsEnabled)) + .filter( + x => + ((x.inlayHintInfo.kind == sihkType) and configuration.typeHintsEnabled) or ( + (x.inlayHintInfo.kind == sihkException) and + configuration.exceptionHintsEnabled + ) or ( + (x.inlayHintInfo.kind == sihkParameter) and + configuration.parameterHintsEnabled + ) + ) .map(x => x.inlayHintInfo.toInlayHint(configuration)) .filter(x => x.label != "") -proc codeAction*(ls: LanguageServer, params: CodeActionParams): - Future[seq[CodeAction]] {.async.} = +proc codeAction*( + ls: LanguageServer, params: CodeActionParams +): Future[seq[CodeAction]] {.async.} = let projectUri = await getProjectFile(params.textDocument.uri.uriToPath, ls) - return seq[CodeAction] %* [{ - "title": "Clean build", - "kind": "source", - "command": { - "title": "Clean build", - "command": RECOMPILE_COMMAND, - "arguments": @[projectUri] - } - }, { - "title": "Refresh project errors", - "kind": "source", - "command": { - "title": "Refresh project errors", - "command": CHECK_PROJECT_COMMAND, - "arguments": @[projectUri] - } - }, { - "title": "Restart nimsuggest", - "kind": "source", - "command": { - "title": "Restart nimsuggest", - "command": RESTART_COMMAND, - "arguments": @[projectUri] - } - }] - -proc executeCommand*(ls: LanguageServer, params: ExecuteCommandParams): - Future[JsonNode] {.async.} = + return + seq[CodeAction] %* [ + { + "title": "Clean build", + "kind": "source", + "command": { + "title": "Clean build", + "command": RECOMPILE_COMMAND, + "arguments": @[projectUri], + }, + }, + { + "title": "Refresh project errors", + "kind": "source", + "command": { + "title": "Refresh project errors", + "command": CHECK_PROJECT_COMMAND, + "arguments": @[projectUri], + }, + }, + { + "title": "Restart nimsuggest", + "kind": "source", + "command": { + "title": "Restart nimsuggest", + "command": RESTART_COMMAND, + "arguments": @[projectUri], + }, + }, + ] + +proc executeCommand*( + ls: LanguageServer, params: ExecuteCommandParams +): Future[JsonNode] {.async.} = let projectFile = params.arguments[0].getStr - case params.command: + case params.command of RESTART_COMMAND: debug "Restarting nimsuggest", projectFile = projectFile ls.createOrRestartNimsuggest(projectFile, projectFile.pathToUri) @@ -545,11 +607,10 @@ proc executeCommand*(ls: LanguageServer, params: ExecuteCommandParams): if ns != nil: ls.workDoneProgressCreate(token) ls.progress(token, "begin", fmt "Compiling project {projectFile}") - ns.await() - .recompile() - .addCallback() do (): - ls.progress(token, "end") - ls.checkProject(projectFile.pathToUri).traceAsyncErrors + + ns.await().recompile().addCallback do(): + ls.progress(token, "end") + ls.checkProject(projectFile.pathToUri).traceAsyncErrors result = newJNull() @@ -567,117 +628,130 @@ proc toSignatureInformation(suggest: Suggest): SignatureInformation = var label = name if detail.len > 1: label = &"{fnKind} {name}({strParams})" - return SignatureInformation %* { - "label": label, - "documentation": suggest.doc, - "parameters": newSeq[ParameterInformation](), #notice params is not used + return + SignatureInformation %* { + "label": label, + "documentation": suggest.doc, + "parameters": newSeq[ParameterInformation](), #notice params is not used } -proc signatureHelp*(ls: LanguageServer, params: SignatureHelpParams, id: int): - Future[Option[SignatureHelp]] {.async.} = - #TODO handle prev signature - # if params.context.activeSignatureHelp.isSome: - # let prevSignature = params.context.activeSignatureHelp.get.signatures.get[params.context.activeSignatureHelp.get.activeSignature.get] - # debug "prevSignature ", prevSignature = $prevSignature.label - # else: - # debug "no prevSignature" - #only support signatureHelp if the client supports it - # if docCaps.signatureHelp.isSome and docCaps.signatureHelp.get.contextSupport.get(false): - # result.capabilities.signatureHelpProvider = SignatureHelpOptions( - # triggerCharacters: some(@["(", ","]) - # ) - if not ls.clientCapabilities.supportSignatureHelp(): +proc signatureHelp*( + ls: LanguageServer, params: SignatureHelpParams, id: int +): Future[Option[SignatureHelp]] {.async.} = + #TODO handle prev signature + # if params.context.activeSignatureHelp.isSome: + # let prevSignature = params.context.activeSignatureHelp.get.signatures.get[params.context.activeSignatureHelp.get.activeSignature.get] + # debug "prevSignature ", prevSignature = $prevSignature.label + # else: + # debug "no prevSignature" + #only support signatureHelp if the client supports it + # if docCaps.signatureHelp.isSome and docCaps.signatureHelp.get.contextSupport.get(false): + # result.capabilities.signatureHelpProvider = SignatureHelpOptions( + # triggerCharacters: some(@["(", ","]) + # ) + if not ls.clientCapabilities.supportSignatureHelp(): #Some clients doesnt support signatureHelp + return none[SignatureHelp]() + with (params.position, params.textDocument): + asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) + let nimsuggest = await ls.tryGetNimsuggest(uri) + if nimsuggest.isNone: + return none[SignatureHelp]() + if nsCon notin nimSuggest.get.capabilities: + #support signatureHelp only if the current version of NimSuggest supports it. + return none[SignatureHelp]() + + let completions = await nimsuggest.get.con( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) + let signatures = completions.map(toSignatureInformation) + if signatures.len() > 0: + return some SignatureHelp( + signatures: some(signatures), activeSignature: some(0), activeParameter: some(0) + ) + else: return none[SignatureHelp]() - with (params.position, params.textDocument): - asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) - let nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone: return none[SignatureHelp]() - if nsCon notin nimSuggest.get.capabilities: - #support signatureHelp only if the current version of NimSuggest supports it. - return none[SignatureHelp]() - - let completions = await nimsuggest.get - .con(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) - let signatures = completions.map(toSignatureInformation); - if signatures.len() > 0: - return some SignatureHelp( - signatures: some(signatures), - activeSignature: some(0), - activeParameter: some(0) - ) - else: - return none[SignatureHelp]() +proc format*( + ls: LanguageServer, nphPath, uri: string +): Future[Option[TextEdit]] {.async.} = + let filePath = ls.uriStorageLocation(uri) + if not fileExists(filePath): + warn "File doenst exist ", filePath = filePath, uri = uri + return none(TextEdit) -proc format*(nphPath, filePath: string): Future[Option[TextEdit]] {.async.} = debug "nph starts", nphPath = nphPath, filePath = filePath - let process = await startProcess(nphPath, arguments = @[filePath], options = { UsePath }) + let process = await startProcess( + nphPath, + arguments = @[filePath], + options = {UsePath}, + stderrHandle = AsyncProcess.Pipe, + ) let res = await process.waitForExit(InfiniteDuration) if res != 0: - error "There was an error trying to format the document. " - #TODO show notification to the user containing the error. + let err = string.fromBytes(process.stderrStream.read().await) + error "There was an error trying to format the document. ", err = err + ls.showMessage(&"Error formating {uri}:{err}", MessageType.Error) return none(TextEdit) + debug "nph completes ", res = res let formattedText = readFile(filePath) let fullRange = Range( - start: Position(line: 0, character: 0), - `end`: Position(line: int.high, character: int.high) - ) + start: Position(line: 0, character: 0), + `end`: Position(line: int.high, character: int.high), + ) some TextEdit(range: fullRange, newText: formattedText) -proc formatting*(ls: LanguageServer, params: DocumentFormattingParams, id: int): - Future[seq[TextEdit]] {.async.} = - with (params.textDocument): +proc formatting*( + ls: LanguageServer, params: DocumentFormattingParams, id: int +): Future[seq[TextEdit]] {.async.} = + with (params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) - debug "Received Formatting request ", filePath = ls.uriStorageLocation(uri), params = params[] - let filePath = ls.uriStorageLocation(uri) - if not fileExists(filePath): - warn "File doenst exist ", filePath = filePath, uri = uri - return @[] - let formatTextEdit = await format(getNphPath().get(), filePath) + debug "Received Formatting request " + let formatTextEdit = await ls.format(getNphPath().get(), uri) if formatTextEdit.isSome: return @[formatTextEdit.get] -proc workspaceSymbol*(ls: LanguageServer, params: WorkspaceSymbolParams, id: int): - Future[seq[SymbolInformation]] {.async.} = +proc workspaceSymbol*( + ls: LanguageServer, params: WorkspaceSymbolParams, id: int +): Future[seq[SymbolInformation]] {.async.} = if ls.lastNimsuggest != nil: let nimsuggest = await ls.lastNimsuggest - symbols = await nimsuggest - .globalSymbols(params.query, "-") - return symbols.map(toSymbolInformation); + symbols = await nimsuggest.globalSymbols(params.query, "-") + return symbols.map(toSymbolInformation) proc toDocumentHighlight(suggest: Suggest): DocumentHighlight = - return DocumentHighlight %* { - "range": toLabelRange(suggest) - } - -proc documentHighlight*(ls: LanguageServer, params: TextDocumentPositionParams, id: int): - Future[seq[DocumentHighlight]] {.async.} = + return DocumentHighlight %* {"range": toLabelRange(suggest)} +proc documentHighlight*( + ls: LanguageServer, params: TextDocumentPositionParams, id: int +): Future[seq[DocumentHighlight]] {.async.} = with (params.position, params.textDocument): asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri) - let - nimsuggest = await ls.tryGetNimsuggest(uri) - if nimsuggest.isNone: return @[] - let - suggestLocations = await nimsuggest.get.highlight(uriToPath(uri), - ls.uriToStash(uri), - line + 1, - ls.getCharacter(uri, line, character)) - result = suggestLocations.map(toDocumentHighlight); + let nimsuggest = await ls.tryGetNimsuggest(uri) + if nimsuggest.isNone: + return @[] + let suggestLocations = await nimsuggest.get.highlight( + uriToPath(uri), + ls.uriToStash(uri), + line + 1, + ls.getCharacter(uri, line, character), + ) + result = suggestLocations.map(toDocumentHighlight) -proc extractId (id: JsonNode): int = +proc extractId(id: JsonNode): int = if id.kind == JInt: result = id.getInt 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, gcsafe.} = debug "Shutting down" await ls.stopNimsuggestProcesses() ls.isShutdown = true @@ -685,8 +759,9 @@ proc shutdown*(ls: LanguageServer, input: JsonNode): Future[JsonNode] {.async, g result = newJNull() trace "Shutdown complete" -proc exit*(p: tuple[ls: LanguageServer, onExit: OnExitCallback], _: JsonNode): - Future[JsonNode] {.async, gcsafe .} = +proc exit*( + p: tuple[ls: LanguageServer, onExit: OnExitCallback], _: JsonNode +): Future[JsonNode] {.async, gcsafe.} = if not p.ls.isShutdown: debug "Received an exit request without prior shutdown request" await p.ls.stopNimsuggestProcesses() @@ -695,62 +770,86 @@ proc exit*(p: tuple[ls: LanguageServer, onExit: OnExitCallback], _: JsonNode): await p.onExit() #Notifications -proc initialized*(ls: LanguageServer, _: JsonNode): - Future[void] {.async.} = +proc initialized*(ls: LanguageServer, _: JsonNode): Future[void] {.async.} = debug "Client initialized." maybeRegisterCapabilityDidChangeConfiguration(ls) maybeRequestConfigurationFromClient(ls) -proc cancelRequest*(ls: LanguageServer, params: CancelParams): - Future[void] {.async.} = +proc cancelRequest*(ls: LanguageServer, params: CancelParams): Future[void] {.async.} = if params.id.isSome: let id = params.id.get.getInt.uint - if id notin ls.pendingRequests: return + if id notin ls.pendingRequests: + return let pendingRequest = ls.pendingRequests[id] if ls.pendingRequests[id].request != nil: - debug "Cancelling: ", id = id - await ls.pendingRequests[id].request.cancelAndWait() + debug "Cancelling: ", id = id + await ls.pendingRequests[id].request.cancelAndWait() ls.pendingRequests[id].state = prsCancelled ls.pendingRequests[id].endTime = now() proc setTrace*(ls: LanguageServer, params: SetTraceParams) {.async.} = debug "setTrace", value = params.value -proc didChange*(ls: LanguageServer, params: DidChangeTextDocumentParams): - Future[void] {.async, gcsafe.} = - with params: - let uri = textDocument.uri - if uri notin ls.openFiles: - return - let - file = open(ls.uriStorageLocation(uri), fmWrite) - - ls.openFiles[uri].fingerTable = @[] - ls.openFiles[uri].changed = true - if contentChanges.len <= 0: - file.close() - return - for line in contentChanges[0].text.splitLines: - ls.openFiles[uri].fingerTable.add line.createUTFMapping() - file.writeLine line +proc didChange*( + ls: LanguageServer, params: DidChangeTextDocumentParams +): Future[void] {.async, gcsafe.} = + with params: + let uri = textDocument.uri + if uri notin ls.openFiles: + return + let file = open(ls.uriStorageLocation(uri), fmWrite) + + ls.openFiles[uri].fingerTable = @[] + ls.openFiles[uri].changed = true + if contentChanges.len <= 0: file.close() + return + for line in contentChanges[0].text.splitLines: + ls.openFiles[uri].fingerTable.add line.createUTFMapping() + file.writeLine line + file.close() - ls.scheduleFileCheck(uri) + ls.scheduleFileCheck(uri) + +proc autoFormat(ls: LanguageServer, config: NlsConfig, uri: string) {.async.} = + let nphPath = getNphPath() + let shouldAutoFormat = + nphPath.isSome and ls.serverCapabilities.documentFormattingProvider.get(false) and + ls.clientCapabilities.workspace.get(ClientCapabilities_workspace()).applyEdit.get( + false + ) and config.formatOnSave.get(false) + if shouldAutoFormat: + let formatTextEdit = await ls.format(nphPath.get(), uri) + if formatTextEdit.isSome: + let workspaceEdit = WorkspaceEdit( + documentChanges: some @[ + TextDocumentEdit( + textDocument: VersionedTextDocumentIdentifier(uri: uri), + edits: some @[formatTextEdit.get()], + ) + ] + ) + discard await ls.applyEdit(ApplyWorkspaceEditParams(edit: workspaceEdit)) -proc didSave*(ls: LanguageServer, params: DidSaveTextDocumentParams): - Future[void] {.async, gcsafe.} = +proc didSave*( + ls: LanguageServer, params: DidSaveTextDocumentParams +): Future[void] {.async, gcsafe.} = let uri = params.textDocument.uri nimsuggest = ls.tryGetNimsuggest(uri).await() - if nimsuggest.isNone: return - + + let config = await ls.getWorkspaceConfiguration() + asyncSpawn ls.autoFormat(config, uri) + if nimsuggest.isNone: + return + ls.openFiles[uri].changed = false traceAsyncErrors nimsuggest.get.changed(uriToPath(uri)) - if ls.getWorkspaceConfiguration().await().checkOnSave.get(true): + if config.checkOnSave.get(true): debug "Checking project", uri = uri traceAsyncErrors ls.checkProject(uri) - + # var toStop = newTable[string, Nimsuggest]() # #We first get the project file for the current file so we can test if this file recently imported another project # let thisProjectFile = await getProjectFile(uri.uriToPath, ls) @@ -769,9 +868,10 @@ proc didSave*(ls: LanguageServer, params: DidSaveTextDocumentParams): # if toStop.len > 0: # ls.sendStatusChanged() -proc didClose*(ls: LanguageServer, params: DidCloseTextDocumentParams): - Future[void] {.async, gcsafe.} = - let uri = params.textDocument.uri +proc didClose*( + ls: LanguageServer, params: DidCloseTextDocumentParams +): Future[void] {.async, gcsafe.} = + let uri = params.textDocument.uri debug "Closed the following document:", uri = uri if ls.openFiles[uri].changed: @@ -780,31 +880,31 @@ proc didClose*(ls: LanguageServer, params: DidCloseTextDocumentParams): ls.openFiles.del uri -proc didOpen*(ls: LanguageServer, params: DidOpenTextDocumentParams): - Future[void] {.async, gcsafe.} = +proc didOpen*( + ls: LanguageServer, params: DidOpenTextDocumentParams +): Future[void] {.async, gcsafe.} = with params.textDocument: debug "New document opened for URI:", uri = uri let file = open(ls.uriStorageLocation(uri), fmWrite) projectFileFuture = getProjectFile(uriToPath(uri), ls) - ls.openFiles[uri] = NlsFileInfo( - projectFile: projectFileFuture, - changed: false, - fingerTable: @[]) + ls.openFiles[uri] = + NlsFileInfo(projectFile: projectFileFuture, changed: false, fingerTable: @[]) let projectFile = await projectFileFuture - debug "Document associated with the following projectFile", uri = uri, projectFile = projectFile + debug "Document associated with the following projectFile", + uri = uri, projectFile = projectFile if not ls.projectFiles.hasKey(projectFile): debug "Will create nimsuggest for this file", uri = uri - ls.createOrRestartNimsuggest(projectFile, uri) - + ls.createOrRestartNimsuggest(projectFile, uri) + for line in text.splitLines: if uri in ls.openFiles: ls.openFiles[uri].fingerTable.add line.createUTFMapping() file.writeLine line file.close() - ls.tryGetNimSuggest(uri).addCallback() do (fut: Future[Option[Nimsuggest]]) -> void: + ls.tryGetNimSuggest(uri).addCallback do(fut: Future[Option[Nimsuggest]]) -> void: if not fut.failed and fut.read.isSome: discard ls.warnIfUnknown(fut.read.get(), uri, projectFile) @@ -817,9 +917,9 @@ proc didOpen*(ls: LanguageServer, params: DidOpenTextDocumentParams): debug "Opening project file", uri = projectFile, file = uri ls.showMessage(fmt "Opening {uri}", MessageType.Info) - -proc didChangeConfiguration*(ls: LanguageServer, conf: JsonNode): - Future[void] {.async, gcsafe.} = +proc didChangeConfiguration*( + ls: LanguageServer, conf: JsonNode +): Future[void] {.async, gcsafe.} = debug "Changed configuration: ", conf = conf if ls.usePullConfigurationModel: ls.maybeRequestConfigurationFromClient @@ -831,4 +931,3 @@ proc didChangeConfiguration*(ls: LanguageServer, conf: JsonNode): ls.workspaceConfiguration = newFuture[JsonNode]() ls.workspaceConfiguration.complete(conf) handleConfigurationChanges(ls, oldConfiguration, newConfiguration) - \ No newline at end of file