# # # 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: --autobind automatically binds into a free port --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. If --autobind is used, the binded port number will be printed to stdout. 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 gAutoBind = false 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 i < cmd.len and 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.s
discard """
  line: 18
  errormsg: "type mismatch"
"""

type
  TObj = object {.pure, inheritable.}
  TObjB = object of TObj
    a, b, c: string
    fn: proc (): int {.tags: [].}

  EIO2 = ref object of EIO

proc raiser(): int {.tags: [TObj, FWriteIO].} =
  writeLine stdout, "arg"

var o: TObjB
o.fn = raiser
ase 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 += skipWhitespace(cmd, i) if i < cmd.len and cmd[i] in {'0'..'9'}: orig = string conf.projectFull else: i = parseQuoted(cmd, orig, i) if i < cmd.len and cmd[i] == ';': i = parseQuoted(cmd, dirtyfile, i+1) i += skipWhile(cmd, seps, i) var line = 0 var col = -1 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 = myLog 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 "autobind": gMode = mtcp gAutoBind = true 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) when isMainModule: handleCmdLine(newIdentCache(), newConfigRef()) else: export Suggest export IdeCmd export AbsoluteFile type NimSuggest* = ref object graph: ModuleGraph idle: int cachedMsgs: CachedMsgs proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest = var retval: ModuleGraph proc mockCommand(graph: ModuleGraph) = retval = graph 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 = myLog conf.structuredErrorHook = nil # compile the project before showing any input so that we already # can answer questions right away: compileProject(graph) proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) = conf.suggestVersion = 0 let a = unixToNativePath(project) 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 let cache = newIdentCache() conf = newConfigRef() self = NimProg( suggestMode: true, processCmdLine: mockCmdLine, mainCommand: mockCommand ) self.initDefinesProg(conf, "nimsuggest") self.processCmdLineAndProjectPath(conf) if gMode != mstdin: conf.writelnHook = proc (msg: string) = discard # Find Nim's prefix dir. if nimPath == "": 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"" else: conf.prefixDir = AbsoluteDir nimPath #msgs.writelnHook = proc (line: string) = log(line) myLog("START " & conf.projectFull.string) discard self.loadConfigsAndRunMainCommand(cache, conf) if gLogging: for it in conf.searchPaths: log(it.string) retval.doStopCompile = proc (): bool = false return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[]) proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] = var retval: seq[Suggest] = @[] let conf = nimsuggest.graph.config conf.ideCmd = cmd conf.writelnHook = proc (line: string) = retval.add(Suggest(section: ideMsg, doc: line)) conf.suggestionResultHook = proc (s: Suggest) = retval.add(s) conf.writelnHook = proc (s: string) = stderr.write s & "\n" if conf.ideCmd == ideKnown: retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file)))) else: if conf.ideCmd == ideChk: for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev) if conf.ideCmd == ideChk: conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) = retval.add(Suggest(section: ideChk, filePath: toFullPath(conf, info), line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev)) else: conf.structuredErrorHook = nil executeNoHooks(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph) return retval