diff options
Diffstat (limited to 'compiler/nimsuggest/nimsuggest.nim')
-rw-r--r-- | compiler/nimsuggest/nimsuggest.nim | 206 |
1 files changed, 172 insertions, 34 deletions
diff --git a/compiler/nimsuggest/nimsuggest.nim b/compiler/nimsuggest/nimsuggest.nim index 8285d81d9..3b4aa658d 100644 --- a/compiler/nimsuggest/nimsuggest.nim +++ b/compiler/nimsuggest/nimsuggest.nim @@ -9,9 +9,17 @@ ## Nimsuggest is a tool that helps to give editors IDE like capabilities. -import strutils, os, parseopt, parseUtils +import strutils, os, parseopt, parseutils, sequtils, net +# Do NOT import suggest. It will lead to wierd bugs with +# suggestionResultHook, because suggest.nim is included by sigmatch. +# So we import that one instead. import options, commands, modules, sem, passes, passaux, msgs, nimconf, - extccomp, condsyms, lists, net, rdstdin + extccomp, condsyms, lists, net, rdstdin, sexp, sigmatch, ast + +when defined(windows): + import winlean +else: + import posix const Usage = """ Nimsuggest - Tool to give every editor IDE like capabilities for Nim @@ -23,17 +31,20 @@ Options: --address:HOST binds to that address, by default "" --stdin read commands from stdin and write results to stdout instead of using sockets + --epc use emacs epc mode The server then listens to the connection and takes line-based commands. In addition, all command line options of Nim that do not affect code generation are supported. """ +type + Mode = enum mstdin, mtcp, mepc var gPort = 6000.Port gAddress = "" - gUseStdin: bool + gMode: Mode const seps = {':', ';', ' ', '\t'} @@ -42,6 +53,9 @@ const "type 'debug' to toggle debug mode on/off\n" & "type 'terse' to toggle terse mode on/off" +type + EUnexpectedCommand = object of Exception + proc parseQuoted(cmd: string; outp: var string; start: int): int = var i = start i += skipWhitespace(cmd, i) @@ -51,7 +65,96 @@ proc parseQuoted(cmd: string; outp: var string; start: int): int = i += parseUntil(cmd, outp, seps, i) result = i -proc action(cmd: string) = +proc sexp(s: IdeCmd): SexpNode = sexp($s) + +proc sexp(s: TSymKind): SexpNode = sexp($s) + +proc sexp(s: Suggest): SexpNode = + # If you change the oder here, make sure to change it over in + # nim-mode.el too. + result = convertSexp([ + s.section, + s.symkind, + s.qualifiedPath.map(newSString), + s.filePath, + s.forth, + s.line, + s.column, + s.doc + ]) + +proc sexp(s: seq[Suggest]): SexpNode = + result = newSList() + for sug in s: + result.add(sexp(sug)) + +proc listEPC(): SexpNode = + let + argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol)) + docstring = sexp("line starts at 1, column at 0, dirtyfile is optional") + result = newSList() + for command in ["sug", "con", "def", "use"]: + let + cmd = sexp(command) + methodDesc = newSList() + methodDesc.add(cmd) + methodDesc.add(argspecs) + methodDesc.add(docstring) + result.add(methodDesc) + +proc execute(cmd: IdeCmd, file, dirtyfile: string, line, col: int) = + gIdeCmd = cmd + if cmd == ideUse: + modules.resetAllModules() + var isKnownFile = true + let dirtyIdx = file.fileInfoIdx(isKnownFile) + + if dirtyfile.len != 0: msgs.setDirtyFile(dirtyIdx, dirtyfile) + else: msgs.setDirtyFile(dirtyIdx, nil) + + resetModule dirtyIdx + if dirtyIdx != gProjectMainIdx: + resetModule gProjectMainIdx + + gTrackPos = newLineInfo(dirtyIdx, line, col) + gErrorCounter = 0 + if not isKnownFile: + compileProject(dirtyIdx) + else: + compileProject() + +proc executeEPC(cmd: IdeCmd, args: SexpNode) = + let + file = args[0].getStr + line = args[1].getNum + column = args[2].getNum + var dirtyfile = "" + if len(args) > 3: + dirtyfile = args[3].getStr(nil) + execute(cmd, file, dirtyfile, int(line), int(column)) + +proc returnEPC(socket: var Socket, uid: BiggestInt, s: SexpNode, return_symbol = "return") = + let response = $convertSexp([newSSymbol(return_symbol), uid, s]) + socket.send(toHex(len(response), 6)) + socket.send(response) + +proc connectToNextFreePort(server: Socket, host: string, start = 30000): int = + result = start + while true: + try: + server.bindaddr(Port(result), host) + return + except OsError: + when defined(windows): + let checkFor = WSAEADDRINUSE.OSErrorCode + else: + let checkFor = EADDRINUSE.OSErrorCode + if osLastError() != checkFor: + raise getCurrentException() + else: + result += 1 + +proc parseCmdLine(cmd: string) = template toggle(sw) = if sw in gGlobalOptions: excl(gGlobalOptions, sw) @@ -69,9 +172,7 @@ proc action(cmd: string) = of "sug": gIdeCmd = ideSug of "con": gIdeCmd = ideCon of "def": gIdeCmd = ideDef - of "use": - modules.resetAllModules() - gIdeCmd = ideUse + of "use": gIdeCmd = ideUse of "quit": quit() of "debug": toggle optIdeDebug of "terse": toggle optIdeTerse @@ -88,35 +189,20 @@ proc action(cmd: string) = i += skipWhile(cmd, seps, i) i += parseInt(cmd, col, i) - var isKnownFile = true - if orig.len == 0: err() - let dirtyIdx = orig.fileInfoIdx(isKnownFile) - - if dirtyfile.len != 0: msgs.setDirtyFile(dirtyIdx, dirtyfile) - else: msgs.setDirtyFile(dirtyIdx, nil) - - resetModule dirtyIdx - if dirtyIdx != gProjectMainIdx: - resetModule gProjectMainIdx - gTrackPos = newLineInfo(dirtyIdx, line, col-1) - #echo dirtyfile, gDirtyBufferIdx, " project ", gProjectMainIdx - gErrorCounter = 0 - if not isKnownFile: - compileProject(dirtyIdx) - else: - compileProject() + execute(gIdeCmd, orig, dirtyfile, line, col-1) proc serve() = # do not stop after the first error: msgs.gErrorMax = high(int) - if gUseStdin: + case gMode: + of mstdin: echo Help var line = "" while readLineFromStdin("> ", line): - action line + parseCmdLine line echo "" flushFile(stdout) - else: + of mtcp: var server = newSocket() server.bindAddr(gPort, gAddress) var inp = "".TaintedString @@ -130,10 +216,55 @@ proc serve() = accept(server, stdoutSocket) stdoutSocket.readLine(inp) - action inp.string + parseCmdLine inp.string stdoutSocket.send("\c\L") stdoutSocket.close() + of mepc: + var server = newSocket() + let port = connectToNextFreePort(server, "localhost") + var inp = "".TaintedString + server.listen() + echo(port) + var client = newSocket() + # Wait for connection + accept(server, client) + while true: + var sizeHex = "" + if client.recv(sizeHex, 6) != 6: + raise newException(ValueError, "didn't get all the hexbytes") + var size = 0 + if parseHex(sizeHex, size) == 0: + raise newException(ValueError, "invalid size hex: " & $sizeHex) + var messageBuffer = "" + if client.recv(messageBuffer, size) != size: + raise newException(ValueError, "didn't get all the bytes") + let + message = parseSexp($messageBuffer) + messageType = message[0].getSymbol + case messageType: + of "call": + var results: seq[Suggest] = @[] + suggestionResultHook = proc (s: Suggest) = + results.add(s) + + let + uid = message[1].getNum + cmd = parseIdeCmd(message[2].getSymbol) + args = message[3] + executeEPC(cmd, args) + returnEPC(client, uid, sexp(results)) + of "return": + raise newException(EUnexpectedCommand, "no return expected") + of "return-error": + raise newException(EUnexpectedCommand, "no return expected") + of "epc-error": + stderr.writeln("recieved epc error: " & $messageBuffer) + raise newException(IOError, "epc error") + of "methods": + returnEPC(client, message[1].getNum, listEPC()) + else: + raise newException(EUnexpectedCommand, "unexpected call: " & messageType) proc mainCommand = registerPass verbosePass @@ -151,15 +282,22 @@ proc mainCommand = proc processCmdLine*(pass: TCmdLinePass, cmd: string) = var p = parseopt.initOptParser(cmd) - while true: + while true: parseopt.next(p) case p.kind - of cmdEnd: break - of cmdLongoption, cmdShortOption: + of cmdEnd: break + of cmdLongoption, cmdShortOption: case p.key.normalize - of "port": gPort = parseInt(p.val).Port - of "address": gAddress = p.val - of "stdin": gUseStdin = true + of "port": + gPort = parseInt(p.val).Port + gMode = mtcp + of "address": + gAddress = p.val + gMode = mtcp + of "stdin": gMode = mstdin + of "epc": + gMode = mepc + gVerbosity = 0 # Port number gotta be first. else: processSwitch(pass, p) of cmdArgument: options.gProjectName = unixToNativePath(p.key) |