Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows compatibility through multisync #125

Merged
merged 3 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
nimcache/
nimlsp
tests/tnimlsp
4 changes: 2 additions & 2 deletions nimlsp.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ bin = @["nimlsp"]

requires "nim >= 1.0.0"
requires "jsonschema >= 0.2.1"
requires "asynctools"

# nimble test does not work for me out of the box
#task test, "Runs the test suite":
Expand All @@ -21,8 +22,7 @@ task debug, "Builds the language server":
exec "nim c --threads:on -d:nimcore -d:nimsuggest -d:debugCommunication -d:debugLogging -o:nimlsp src/nimlsp"

before test:
if not fileExists("nimlsp"):
PMunch marked this conversation as resolved.
Show resolved Hide resolved
exec "nimble build"
exec "nimble build"

task findNim, "Tries to find the current Nim installation":
echo NimVersion
Expand Down
2 changes: 1 addition & 1 deletion src/config.nims
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import os
import std / [os]

switch "path", getCurrentCompilerExe().parentDir.parentDir
130 changes: 79 additions & 51 deletions src/nimlsp.nim
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import std / [os, osproc, tables, sets, hashes, strutils, uri, algorithm, asyncfile, asyncdispatch, streams]
import nimlsppkg / [baseprotocol, utfmapping, suggestlib, logger]
include nimlsppkg / messages
import algorithm
import streams
import tables
import strutils
import os
import hashes
import sets
import uri
import osproc
import asyncfile, asyncdispatch

const
version = block:
Expand Down Expand Up @@ -37,8 +28,6 @@ for i in 1..paramCount():
infoLog("Argument " & $i & ": " & paramStr(i))

var
ins = newAsyncFile(stdin.getOsFileHandle().AsyncFD)
outs = newAsyncFile(stdout.getOsFileHandle().AsyncFD)
gotShutdown = false
initialized = false
projectFiles = initTable[string, tuple[nimsuggest: NimSuggest, openFiles: OrderedSet[string]]]()
Expand Down Expand Up @@ -133,14 +122,25 @@ proc parseId(node: JsonNode): int =
else:
raise newException(MalformedFrame, "Invalid id node: " & repr(node))

proc respond(request: RequestMessage, data: JsonNode) {.async.} =
await outs.sendJson create(ResponseMessage, "2.0", parseId(request["id"]), some(data), none(ResponseError)).JsonNode
macro multisyncTask(body): untyped =
let arg = body[0][1]
result = quote do:
when `arg` is Stream:
`body`
else:
await `body`

proc respond(outs: Stream | AsyncFile, request: RequestMessage, data: JsonNode) {.multisync.} =
let resp = create(ResponseMessage, "2.0", parseId(request["id"]), some(data), none(ResponseError)).JsonNode
multisyncTask: outs.sendJson resp

proc error(request: RequestMessage, errorCode: int, message: string, data: JsonNode) {.async.} =
await outs.sendJson create(ResponseMessage, "2.0", parseId(request["id"]), none(JsonNode), some(create(ResponseError, errorCode, message, data))).JsonNode
proc error(outs: Stream | AsyncFile,request: RequestMessage, errorCode: int, message: string, data: JsonNode) {.multisync.} =
let resp = create(ResponseMessage, "2.0", parseId(request["id"]), none(JsonNode), some(create(ResponseError, errorCode, message, data))).JsonNode
multisyncTask: outs.sendJson resp

proc notify(notification: string, data: JsonNode) {.async.} =
await outs.sendJson create(NotificationMessage, "2.0", notification, some(data)).JsonNode
proc notify(outs: Stream | AsyncFile,notification: string, data: JsonNode) {.multisync.} =
let resp = create(NotificationMessage, "2.0", notification, some(data)).JsonNode
multisyncTask: outs.sendJson resp

type Certainty = enum
None,
Expand Down Expand Up @@ -203,28 +203,31 @@ if not fileExists(nimpath / "config/nim.cfg"):
"Supply the Nim project folder by adding it as an argument.\n"
quit 1

proc main(){.async.} =
proc main(ins: Stream | AsyncFile, outs: Stream | AsyncFile) {.multisync.} =
while true:
try:
debugLog "Trying to read frame"
let frame = await ins.readFrame
let frame = multisyncTask: ins.readFrame
debugLog "Got frame:"
infoLog frame
let message = frame.parseJson
whenValidStrict(message, RequestMessage):
debugLog "Got valid Request message of type " & message["method"].getStr
if not initialized and message["method"].getStr != "initialize":
await message.error(-32002, "Unable to accept requests before being initialized", newJNull())
multisyncTask:
outs.error(message, -32002, "Unable to accept requests before being initialized", newJNull())
continue
case message["method"].getStr:
of "shutdown":
debugLog "Got shutdown request, answering"
await message.respond(newJNull())
let resp = newJNull()
multisyncTask:
outs.respond(message, resp)
gotShutdown = true
of "initialize":
debugLog "Got initialize request, answering"
initialized = true
await message.respond(create(InitializeResult, create(ServerCapabilities,
let resp = create(InitializeResult, create(ServerCapabilities,
textDocumentSync = some(create(TextDocumentSyncOptions,
openClose = some(true),
change = some(TextDocumentSyncKind.Full.int),
Expand Down Expand Up @@ -258,7 +261,9 @@ proc main(){.async.} =
executeCommandProvider = none(ExecuteCommandOptions), #?: ExecuteCommandOptions
workspace = none(WorkspaceCapability), #?: WorkspaceCapability
experimental = none(JsonNode) #?: any
)).JsonNode)
)).JsonNode
multisyncTask:
outs.respond(message,resp)
of "textDocument/completion":
message.textDocumentRequest(CompletionParams, compRequest):
debugLog "Running equivalent of: sug ", uriToPath(fileuri), ";", filestash, ":",
Expand Down Expand Up @@ -306,7 +311,8 @@ proc main(){.async.} =
command = none(Command),
data = none(JsonNode)
).JsonNode
await message.respond completionItems
multisyncTask:
outs.respond(message, completionItems)
of "textDocument/hover":
message.textDocumentRequest(TextDocumentPositionParams, hoverRequest):
debugLog "Running equivalent of: def ", uriToPath(fileuri), ";", filestash, ":",
Expand All @@ -319,8 +325,9 @@ proc main(){.async.} =
debugLog "Found suggestions: ",
suggestions[0..(if suggestions.len > 10: 10 else: suggestions.high)],
(if suggestions.len > 10: " and " & $(suggestions.len-10) & " more" else: "")
var resp: JsonNode
if suggestions.len == 0:
await message.respond newJNull()
resp = newJNull()
else:
var label = suggestions[0].qualifiedPath.join(".")
if suggestions[0].forth != "":
Expand All @@ -333,15 +340,17 @@ proc main(){.async.} =
))
markedString = create(MarkedStringOption, "nim", label)
if suggestions[0].doc != "":
await message.respond create(Hover,
resp = create(Hover,
@[
markedString,
create(MarkedStringOption, "", suggestions[0].nimDocstring),
],
rangeopt
).JsonNode
else:
await message.respond create(Hover, markedString, rangeopt).JsonNode
resp = create(Hover, markedString, rangeopt).JsonNode;
multisyncTask:
outs.respond(message, resp)
of "textDocument/references":
message.textDocumentRequest(ReferenceParams, referenceRequest):
debugLog "Running equivalent of: use ", uriToPath(fileuri), ";", filestash, ":",
Expand All @@ -365,9 +374,12 @@ proc main(){.async.} =
)
).JsonNode
if response.len == 0:
await message.respond newJNull()
multisyncTask:
outs.respond(message, newJNull())
else:
await message.respond response
multisyncTask:
outs.respond(message, response)

of "textDocument/rename":
message.textDocumentRequest(RenameParams, renameRequest):
debugLog "Running equivalent of: use ", uriToPath(fileuri), ";", filestash, ":",
Expand All @@ -380,8 +392,9 @@ proc main(){.async.} =
debugLog "Found suggestions: ",
suggestions[0..(if suggestions.len > 10: 10 else: suggestions.high)],
(if suggestions.len > 10: " and " & $(suggestions.len-10) & " more" else: "")
var resp: JsonNode
if suggestions.len == 0:
await message.respond newJNull()
resp = newJNull()
else:
var textEdits = newJObject()
for suggestion in suggestions:
Expand All @@ -394,10 +407,11 @@ proc main(){.async.} =
),
renameRequest["newName"].getStr
).JsonNode
await message.respond create(WorkspaceEdit,
resp = create(WorkspaceEdit,
some(textEdits),
none(seq[TextDocumentEdit])
).JsonNode
multisyncTask: outs.respond(message, resp)
of "textDocument/definition":
message.textDocumentRequest(TextDocumentPositionParams, definitionRequest):
debugLog "Running equivalent of: def ", uriToPath(fileuri), ";", filestash, ":",
Expand All @@ -410,34 +424,37 @@ proc main(){.async.} =
debugLog "Found suggestions: ",
declarations[0..(if declarations.len > 10: 10 else: declarations.high)],
(if declarations.len > 10: " and " & $(declarations.len-10) & " more" else: "")
var resp: JsonNode
if declarations.len == 0:
await message.respond newJNull()
resp = newJNull()
else:
var response = newJarray()
resp = newJarray()
for declaration in declarations:
response.add create(Location,
resp.add create(Location,
"file://" & pathToUri(declaration.filepath),
create(Range,
create(Position, declaration.line-1, declaration.column),
create(Position, declaration.line-1, declaration.column + declaration.qualifiedPath[^1].len)
)
).JsonNode
await message.respond response
multisyncTask:
outs.respond(message, resp)
of "textDocument/documentSymbol":
message.textDocumentRequest(DocumentSymbolParams, symbolRequest):
debugLog "Running equivalent of: outline ", uriToPath(fileuri), ";", filestash
let syms = getNimsuggest(fileuri).outline(uriToPath(fileuri), dirtyfile = filestash)
debugLog "Found outlines: ",
syms[0..(if syms.len > 10: 10 else: syms.high)],
(if syms.len > 10: " and " & $(syms.len-10) & " more" else: "")
var resp: JsonNode
if syms.len == 0:
await message.respond newJNull()
resp = newJNull()
else:
var response = newJarray()
resp = newJarray()
for sym in syms.sortedByIt((it.line,it.column,it.quality)):
if sym.qualifiedPath.len != 2:
continue
response.add create(
resp.add create(
SymbolInformation,
sym.name[],
nimSymToLSPKind(sym.symKind).int,
Expand All @@ -451,7 +468,8 @@ proc main(){.async.} =
),
none(string)
).JsonNode
await message.respond response
multisyncTask:
outs.respond(message, resp)
of "textDocument/signatureHelp":
message.textDocumentRequest(TextDocumentPositionParams, sigHelpRequest):
debugLog "Running equivalent of: con ", uriToPath(fileuri), ";", filestash, ":",
Expand All @@ -468,15 +486,17 @@ proc main(){.async.} =
documentation = some(suggestion.nimDocstring),
parameters = none(seq[ParameterInformation])
)

await message.respond create(SignatureHelp,
let resp = create(SignatureHelp,
signatures = signatures,
activeSignature = some(0),
activeParameter = some(0)
).JsonNode
multisyncTask:
outs.respond(message, resp)
else:
debugLog "Unknown request"
await message.error(errorCode = -32600, message = "Unknown request: " & frame, data = newJObject())
multisyncTask:
outs.error(message, errorCode = -32600, message = "Unknown request: " & frame, data = newJObject())
continue
whenValidStrict(message, NotificationMessage):
debugLog "Got valid Notification message of type " & message["method"].getStr
Expand Down Expand Up @@ -613,15 +633,14 @@ proc main(){.async.} =
message,
none(seq[DiagnosticRelatedInformation])
)

await notify(
"textDocument/publishDiagnostics",
create(PublishDiagnosticsParams, f, response).JsonNode
)
await notify("textDocument/publishDiagnostics", create(PublishDiagnosticsParams,
let resp = create(PublishDiagnosticsParams, f, response).JsonNode
multisyncTask:
outs.notify("textDocument/publishDiagnostics", resp)
let resp = create(PublishDiagnosticsParams,
fileuri,
response).JsonNode
)
multisyncTask:
outs.notify("textDocument/publishDiagnostics", resp)
else:
warnLog "Got unknown notification message"
continue
Expand All @@ -635,4 +654,13 @@ proc main(){.async.} =
warnLog "Got exception: ", e.msg
continue

waitFor main()
when defined(windows):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason why you've chosen to pass these in to main and therefore pass them around everywhere? You could just as easily have defined ins and outs as global variables like this as before, and then have the multisyncTask check the global variables without having to pass the extra argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multisync may complain about that, I have face some error, multisync not work properly, and it's better for unit test functions.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multisync shouldn't complain, but I get the point about unit testing. I just don't like the repetition in multisyncTask outs: outs.something. But I guess without having them as global the only option would be a macro that inserted the left hand side as the first argument so you could at least write outs.multisyncTask something()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I have to take time recall my macro knowledge , now dont need extra outs pass in macro multisyncTask: outs.sendJson resp

var
ins = newFileStream(stdin)
outs = newFileStream(stdout)
main(ins, outs)
else:
var
ins = newAsyncFile(stdin.getOsFileHandle().AsyncFD)
outs = newAsyncFile(stdout.getOsFileHandle().AsyncFD)
waitFor main(ins, outs)
45 changes: 30 additions & 15 deletions src/nimlsppkg/baseprotocol.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import strutils, parseutils, json
import asyncfile, asyncdispatch
import std / [strutils, parseutils, json, asyncfile, asyncdispatch, streams]
import logger

type
BaseProtocolError* = object of Defect

Expand All @@ -12,23 +12,38 @@ proc skipWhitespace(x: string, pos: int): int =
while result < x.len and x[result] in Whitespace:
inc result

proc sendFrame*(s: AsyncFile, frame: string) {.async} =
proc sendFrame*(s: Stream | AsyncFile, frame: string) {.multisync} =
when defined(debugCommunication):
infoLog(frame)
await s.write "Content-Length: " & $frame.len & "\r\n\r\n" & frame
let content = "Content-Length: " & $frame.len & "\r\n\r\n" & frame
when s is Stream:
s.write content
s.flush
else:
await s.write content

proc formFrame*( data: JsonNode): string =
var frame = newStringOfCap(1024)
toUgly(frame, data)
result = "Content-Length: " & $frame.len & "\r\n\r\n" & frame

proc sendJson*(s: AsyncFile, data: JsonNode) {.async.} =
proc sendJson*(s: Stream | AsyncFile, data: JsonNode) {.multisync.} =
var frame = newStringOfCap(1024)
toUgly(frame, data)
await s.sendFrame(frame)
when s is Stream:
s.sendFrame(frame)
else:
await s.sendFrame(frame)

proc readFrame*(s: AsyncFile): Future[string] {.async.} =
proc readFrame*(s: Stream | AsyncFile): Future[string] {.multisync.} =
var contentLen = -1
var headerStarted = false

var ln: string
while true:
var ln = await s.readLine()

when s is Stream:
ln = s.readLine()
else:
ln = await s.readLine()
if ln.len != 0:
headerStarted = true
let sep = ln.find(':')
Expand All @@ -52,11 +67,11 @@ proc readFrame*(s: AsyncFile): Future[string] {.async.} =
continue
else:
if contentLen != -1:
var buf = newString(contentLen)
var i = 0
while i < contentLen:
let r = await s.readBuffer(buf[i].addr, contentLen - i)
i += r
when s is Stream:
var buf = s.readStr(contentLen)
else:
var buf = newString(contentLen)
discard await s.readBuffer(buf[0].addr, contentLen)
when defined(debugCommunication):
infoLog(buf)
return buf
Expand Down
Loading