# # # The Nim Compiler # (c) Copyright 2017 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## Nimsuggest is a tool that helps to give editors IDE like capabilities. when not defined(nimcore): {.error: "nimcore MUST be defined for Nim's core tooling".} import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp # 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 compiler / [options, commands, modules, sem, passes, passaux, msgs, nimconf, extccomp, condsyms, sigmatch, ast, scriptconfig, idents, modulegraphs, vm, prefixmatches, lineinfos, cmdlinehelper, pathutils] when defined(windows): import winlean else: import posix const DummyEof = "!EOF!" const Usage = """ Nimsuggest - Tool to give every editor IDE like capabilities for Nim Usage: nimsuggest [options] projectfile.nim Options: --port:PORT port, by default 6000 --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 --debug enable debug output --log enable verbose logging to nimsuggest.log file --v1 use version 1 of the protocol; for backwards compatibility --refresh perform automatic refreshes to keep the analysis precise --maxresults:N limit the number of suggestions to N --tester implies --stdin and outputs a line '""" & DummyEof & """' for the tester 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, mcmdsug, mcmdcon CachedMsg = object info: TLineInfo msg: string sev: Severity CachedMsgs = seq[CachedMsg] var gPort = 6000.Port gAddress = "" gMode: Mode gEmitEof: bool # whether we write '!EOF!' dummy lines gLogging = defined(logging) gRefresh: bool requests: Channel[string] results: Channel[Suggest] proc writelnToChannel(line: string) = results.send(Suggest(section: ideMsg, doc: line)) proc sugResultHook(s: Suggest) = results.send(s) proc errorHook(conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) = results.send(Suggest(section: ideChk, filePath: toFullPath(conf, info), line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev)) proc myLog(s: string) = if gLogging: log(s) const seps = {':', ';', ' ', '\t'} Help = "usage: sug|con|def|use|dus|chk|mod|highlight|outline|known file.nim[;dirtyfile.nim]:line:col\n" & "type 'quit' to quit\n" & "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) if cmd[i] == '"': i += parseUntil(cmd, outp, '"', i+1)+2 else: i += parseUntil(cmd, outp, seps, i) result = i proc sexp(s: IdeCmd|TSymKind|PrefixMatch): SexpNode = sexp($s) proc sexp(s: Suggest): SexpNode = # If you change the order here, make sure to change it over in # nim-mode.el too. let qp = if s.qualifiedPath.len == 0: @[] else: s.qualifiedPath result = convertSexp([ s.section, TSymKind s.symkind, qp.map(newSString), s.filePath, s.forth, s.line, s.column, s.doc, s.quality ]) if s.section == ideSug: result.add convertSexp(s.prefix) proc sexp(s: seq[Suggest]): SexpNode = result = newSList() for sug in s: result.add(sexp(sug)) proc listEpc(): SexpNode = # This function is called from Emacs to show available options. 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", "dus", "chk", "mod"]: let cmd = sexp(command) methodDesc = newSList() methodDesc.add(cmd) methodDesc.add(argspecs) methodDesc.add(docstring) result.add(methodDesc) proc findNode(n: PNode; trackPos: TLineInfo): PSym = #echo "checking node ", n.info if n.kind == nkSym: if isTracked(n.info, trackPos, n.sym.name.s.len): return n.sym else: for i in 0 ..< safeLen(n): let res = findNode(n[i], trackPos) if res != nil: return res proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo): PSym = let m = graph.getModule(trackPos.fileIndex) if m != nil and m.ast != nil: result = findNode(m.ast, trackPos) proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; graph: ModuleGraph) = let conf = graph.config myLog("cmd: " & $cmd & ", file: " & file.string & ", dirtyFile: " & dirtyfile.string & "[" & $line & ":" & $col & "]") conf.ideCmd = cmd if cmd == ideChk: conf.structuredErrorHook = errorHook conf.writelnHook = myLog else: conf.structuredErrorHook = nil conf.writelnHook = myLog if cmd == ideUse and conf.suggestVersion != 0: graph.resetAllModules() var isKnownFile = true let dirtyIdx = fileInfoIdx(conf, file, isKnownFile) if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile) else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"") conf.m.trackPos = newLineInfo(dirtyIdx, line, col) conf.m.trackPosAttached = false conf.errorCounter = 0 if conf.suggestVersion == 1: graph.usageSym = nil if not isKnownFile: graph.compileProject() if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and dirtyfile.isEmpty: discard "no need to recompile anything" else: let modIdx = graph.parentModule(dirtyIdx) graph.markDirty dirtyIdx graph.markClientsDirty dirtyIdx if conf.ideCmd != ideMod: graph.compileProject(modIdx) if conf.ideCmd in {ideUse, ideDus}: let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym if u != nil: listUsages(conf, u) else: localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos)) proc executeEpc(cmd: IdeCmd, args: SexpNode; graph: ModuleGraph) = let file = AbsoluteFile args[0].getStr line = args[1].getNum column = args[2].getNum var dirtyfile = AbsoluteFile"" if len(args) > 3: dirtyfile = AbsoluteFile args[3].getStr("") execute(cmd, file, dirtyfile, int(line), int(column), graph) proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string, return_symbol = "return") = let response = $convertSexp([newSSymbol(return_symbol), uid, s]) socket.send(toHex(len(response), 6)) socket.send(response) template checkSanity(client, sizeHex, size, messageBuffer: typed) = if client.recv(sizeHex, 6) != 6: raise newException(ValueError, "didn't get all the hexbytes") if parseHex(sizeHex, size) == 0: raise newException(ValueError, "invalid size hex: " & $sizeHex) if client.recv(messageBuffer, size) != size: raise newException(ValueError, "didn't get all the bytes") proc toStdout() {.gcsafe.} = while true: let res = results.recv() case res.section of ideNone: break of ideMsg: echo res.doc of ideKnown: echo res.quality == 1 else: echo res proc toSocket(stdoutSocket: Socket) {.gcsafe.} = while true: let res = results.recv() case res.section of ideNone: break of ideMsg: stdoutSocket.send(res.doc & "\c\L") of ideKnown: stdoutSocket.send($(res.quality == 1) & "\c\L") else: stdoutSocket.send($res & "\c\L") proc toEpc(client: Socket; uid: BiggestInt) {.gcsafe.} = var list = newSList() while true: let res = results.recv() case res.section of ideNone: break of ideMsg: list.add sexp(res.doc) of ideKnown: list.add sexp(res.quality == 1) else: list.add sexp(res) returnEpc(client, uid, list) template setVerbosity(level: typed) = gVerbosity = level conf.notes = NotesVerbosity[gVerbosity] proc connectToNextFreePort(server: Socket, host: string): Port = server.bindaddr(Port(0), host) let (_, port) = server.getLocalAddr result = port type ThreadParams = tuple[port: Port; address: string] proc replStdinSingleCmd(line: string) = requests.send line toStdout() echo "" flushFile(stdout) proc replStdin(x: ThreadParams) {.thread.} = if gEmitEof: echo DummyEof while true: let line = readLine(stdin) requests.send line if line == "quit": break toStdout() echo DummyEof flushFile(stdout) else: echo Help var line = "" while readLineFromStdin("> ", line): replStdinSingleCmd(line) requests.send "quit" proc replCmdline(x: ThreadParams) {.thread.} = replStdinSingleCmd(x.address) requests.send "quit" proc replTcp(x: ThreadParams) {.thread.} = var server = newSocket() server.bindAddr(x.port, x.address) var inp = "".TaintedString server.listen() while true: var stdoutSocket = newSocket() accept(server, stdoutSocket) stdoutSocket.readLine(inp) requests.send inp toSocket(stdoutSocket) stdoutSocket.send("\c\L") stdoutSocket.close() proc argsToStr(x: SexpNode): string = if x.kind != SList: return x.getStr doAssert x.kind == SList doAssert x.len >= 4 let file = x[0].getStr let line = x[1].getNum let col = x[2].getNum let dirty = x[3].getStr result = x[0].getStr.escape if dirty.len > 0: result.add ';' result.add dirty.escape result.add ':' result.add line result.add ':' result.add col proc replEpc(x: ThreadParams) {.thread.} = var server = newSocket() let port = connectToNextFreePort(server, "localhost") server.listen() echo port stdout.flushFile() var client = newSocket() # Wait for connection accept(server, client) while true: var sizeHex = "" size = 0 messageBuffer = "" checkSanity(client, sizeHex, size, messageBuffer) let message = parseSexp($messageBuffer) epcApi = message[0].getSymbol case epcApi of "call": let uid = message[1].getNum cmd = message[2].getSymbol args = message[3] when false: x.ideCmd[] = parseIdeCmd(message[2].getSymbol) case x.ideCmd[] of ideSug, ideCon, ideDef, ideUse, ideDus, ideOutline, ideHighlight: setVerbosity(0) else: discard let fullCmd = cmd & " " & args.argsToStr myLog "MSG CMD: " & fullCmd requests.send(fullCmd) toEpc(client, uid) of "methods": returnEpc(client, message[1].getNum, listEpc()) of "epc-error": # an unhandled exception forces down the whole process anyway, so we # use 'quit' here instead of 'raise' quit("received epc error: " & $messageBuffer) else: let errMessage = case epcApi of "return", "return-error": "no return expected" else: "unexpected call: " & epcAPI quit errMessage proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) = let conf = graph.config template sentinel() = # send sentinel for the input reading thread: results.send(Suggest(section: ideNone)) template toggle(sw) = if sw in conf.globalOptions: excl(conf.globalOptions, sw) else: incl(conf.globalOptions, sw) sentinel() return template err() = echo Help sentinel() return var opc = "" var i = parseIdent(cmd, opc, 0) case opc.normalize of "sug": conf.ideCmd = ideSug of "con": conf.ideCmd = ideCon of "def": conf.ideCmd = ideDef of "use": conf.ideCmd = ideUse of "dus": conf.ideCmd = ideDus of "mod": conf.ideCmd = ideMod of "chk": conf.ideCmd = ideChk of "highlight": conf.ideCmd = ideHighlight of "outline": conf.ideCmd = ideOutline of "quit": sentinel() quit() of "debug": toggle optIdeDebug of "terse": toggle optIdeTerse of "known": conf.ideCmd = ideKnown else: err() var dirtyfile = "" var orig = "" i = parseQuoted(cmd, orig, i) if cmd[i] == ';': i = parseQuoted(cmd, dirtyfile, i+1) i += skipWhile(cmd, seps, i) var line = -1 var col = 0 i += parseInt(cmd, line, i) i += skipWhile(cmd, seps, i) i += parseInt(cmd, col, i) if conf.ideCmd == ideKnown: results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig)))) else: if conf.ideCmd == ideChk: for cm in cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev) execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, graph) sentinel() proc recompileFullProject(graph: ModuleGraph) = #echo "recompiling full project" resetSystemArtifacts(graph) graph.vm = nil graph.resetAllModules() GC_fullcollect() compileProject(graph) #echo GC_getStatistics() proc mainThread(graph: ModuleGraph) = let conf = graph.config if gLogging: for it in conf.searchPaths: log(it.string) proc wrHook(line: string) {.closure.} = if gMode == mepc: if gLogging: log(line) else: writelnToChannel(line) conf.writelnHook = wrHook conf.suggestionResultHook = sugResultHook graph.doStopCompile = proc (): bool = requests.peek() > 0 var idle = 0 var cachedMsgs: CachedMsgs = @[] while true: let (hasData, req) = requests.tryRecv() if hasData: conf.writelnHook = wrHook conf.suggestionResultHook = sugResultHook execCmd(req, graph, cachedMsgs) idle = 0 else: os.sleep 250 idle += 1 if idle == 20 and gRefresh: # we use some nimsuggest activity to enable a lazy recompile: conf.ideCmd = ideChk conf.writelnHook = proc (s: string) = discard cachedMsgs.setLen 0 conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) = cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev)) conf.suggestionResultHook = proc (s: Suggest) = discard recompileFullProject(graph) var inputThread: Thread[ThreadParams] proc mainCommand(graph: ModuleGraph) = let conf = graph.config clearPasses(graph) registerPass graph, verbosePass registerPass graph, semPass conf.cmd = cmdIdeTools wantMainModule(conf) if not fileExists(conf.projectFull): quit "cannot find file: " & conf.projectFull.string add(conf.searchPaths, conf.libpath) # do not stop after the first error: conf.errorMax = high(int) # do not print errors, but log them conf.writelnHook = proc (s: string) = log(s) conf.structuredErrorHook = nil # compile the project before showing any input so that we already # can answer questions right away: compileProject(graph) open(requests) open(results) case gMode of mstdin: createThread(inputThread, replStdin, (gPort, gAddress)) of mtcp: createThread(inputThread, replTcp, (gPort, gAddress)) of mepc: createThread(inputThread, replEpc, (gPort, gAddress)) of mcmdsug: createThread(inputThread, replCmdline, (gPort, "sug \"" & conf.projectFull.string & "\":" & gAddress)) of mcmdcon: createThread(inputThread, replCmdline, (gPort, "con \"" & conf.projectFull.string & "\":" & gAddress)) mainThread(graph) joinThread(inputThread) close(requests) close(results) proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) = var p = parseopt.initOptParser(cmd) while true: parseopt.next(p) case p.kind of cmdEnd: break of cmdLongoption, cmdShortOption: case p.key.normalize of "help", "h": stdout.writeline(Usage) quit() of "port": gPort = parseInt(p.val).Port gMode = mtcp of "address": gAddress = p.val gMode = mtcp of "stdin": gMode = mstdin of "cmdsug": gMode = mcmdsug gAddress = p.val incl(conf.globalOptions, optIdeDebug) of "cmdcon": gMode = mcmdcon gAddress = p.val incl(conf.globalOptions, optIdeDebug) of "epc": gMode = mepc conf.verbosity = 0 # Port number gotta be first. of "debug": incl(conf.globalOptions, optIdeDebug) of "v2": conf.suggestVersion = 0 of "v1": conf.suggestVersion = 1 of "tester": gMode = mstdin gEmitEof = true gRefresh = false of "log": gLogging = true of "refresh": if p.val.len > 0: gRefresh = parseBool(p.val) else: gRefresh = true of "maxresults": conf.suggestMaxResults = parseInt(p.val) else: processSwitch(pass, p, conf) of cmdArgument: let a = unixToNativePath(p.key) if dirExists(a) and not fileExists(a.addFileExt("nim")): conf.projectName = findProjectNimFile(conf, a) # don't make it worse, report the error the old way: if conf.projectName.len == 0: conf.projectName = a else: conf.projectName = a # if processArgument(pass, p, argsCount): break proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = let self = NimProg( suggestMode: true, processCmdLine: processCmdLine, mainCommand: mainCommand ) self.initDefinesProg(conf, "nimsuggest") if paramCount() == 0: stdout.writeline(Usage) return self.processCmdLineAndProjectPath(conf) if gMode != mstdin: conf.writelnHook = proc (msg: string) = discard # Find Nim's prefix dir. let binaryPath = findExe("nim") if binaryPath == "": raise newException(IOError, "Cannot find Nim standard library: Nim compiler not in PATH") conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir() if not dirExists(conf.prefixDir / RelativeDir"lib"): conf.prefixDir = AbsoluteDir"" #msgs.writelnHook = proc (line: string) = log(line) myLog("START " & conf.projectFull.string) discard self.loadConfigsAndRunMainCommand(cache, conf) handleCmdline(newIdentCache(), newConfigRef())