diff options
Diffstat (limited to 'nimsuggest/nimsuggest.nim')
-rw-r--r-- | nimsuggest/nimsuggest.nim | 1392 |
1 files changed, 1392 insertions, 0 deletions
diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim new file mode 100644 index 000000000..04bae08c1 --- /dev/null +++ b/nimsuggest/nimsuggest.nim @@ -0,0 +1,1392 @@ +# +# +# The Nim Compiler +# (c) Copyright 2017 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +import compiler/renderer +import compiler/types +import compiler/trees +import compiler/wordrecg +import compiler/sempass2 +import strformat +import algorithm +import tables +import times +import procmonitor + +template tryImport(module) = import module + +when compiles tryImport ../dist/checksums/src/checksums/sha1: + import ../dist/checksums/src/checksums/sha1 +else: + import checksums/sha1 + +## 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 weird bugs with +# suggestionResultHook, because suggest.nim is included by sigmatch. +# So we import that one instead. +import compiler / [options, commands, modules, + passes, passaux, msgs, + sigmatch, ast, + idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper, + pathutils, condsyms, syntaxes, suggestsymdb] + +when defined(nimPreviewSlimSystem): + import std/typedthreads + +when defined(windows): + import winlean +else: + import posix + +const HighestSuggestProtocolVersion = 4 +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 + --clientProcessId:PID shutdown nimsuggest in case this process dies + --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 + --v2 use version 2(default) of the protocol + --v3 use version 3 of the protocol + --v4 use version 4 of the protocol + --info:X information + --info:nimVer return the Nim compiler version that nimsuggest uses internally + --info:protocolVer return the newest protocol version that is supported + --info:capabilities return the capabilities supported by nimsuggest + --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 + --find attempts to find the project file of the current project + --exceptionInlayHints:on|off + globally turn exception inlay hints on|off + +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 = "127.0.0.1" + gMode: Mode + gEmitEof: bool # whether we write '!EOF!' dummy lines + gLogging = defined(logging) + gRefresh: bool + gAutoBind = false + + requests: Channel[string] + results: Channel[Suggest] + +proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string, + graph: ModuleGraph); + +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|project 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" + #List of currently supported capabilities. So lang servers/ides can iterate over and check for what's enabled + Capabilities = [ + "con", #current NimSuggest supports the `con` commmand + "exceptionInlayHints", + "unknownFile", #current NimSuggest can handle unknown files + ] + +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.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) + if s.section in {ideOutline, ideExpand} and s.version == 3: + result.add convertSexp(s.endLine.int) + result.add convertSexp(s.endCol) + +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", "globalSymbols", "recompile", "saved", "chkFile", "declaration", "inlayHints"]: + 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) + +template benchmark(benchmarkName: untyped, code: untyped) = + block: + myLog "Started [" & benchmarkName & "]..." + let t0 = epochTime() + code + let elapsed = epochTime() - t0 + let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 3) + myLog "CPU Time [" & benchmarkName & "] " & elapsedStr & "s" + +proc clearInstCache(graph: ModuleGraph, projectFileIdx: FileIndex) = + if projectFileIdx == InvalidFileIdx: + graph.typeInstCache.clear() + graph.procInstCache.clear() + return + var typeIdsToDelete = newSeq[ItemId]() + for id in graph.typeInstCache.keys: + if id.module == projectFileIdx.int: + typeIdsToDelete.add id + for id in typeIdsToDelete: + graph.typeInstCache.del id + var procIdsToDelete = newSeq[ItemId]() + for id in graph.procInstCache.keys: + if id.module == projectFileIdx.int: + procIdsToDelete.add id + for id in procIdsToDelete: + graph.procInstCache.del id + + for tbl in mitems(graph.attachedOps): + var attachedOpsToDelete = newSeq[ItemId]() + for id in tbl.keys: + if id.module == projectFileIdx.int and sfOverridden in resolveAttachedOp(graph, tbl[id]).flags: + attachedOpsToDelete.add id + for id in attachedOpsToDelete: + tbl.del id + +proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, tag: string, + graph: ModuleGraph) = + let conf = graph.config + + if conf.suggestVersion >= 3: + let command = fmt "cmd = {cmd} {file}:{line}:{col}" + benchmark command: + executeNoHooksV3(cmd, file, dirtyfile, line, col, tag, graph) + return + + myLog("cmd: " & $cmd & ", file: " & file.string & + ", dirtyFile: " & dirtyfile.string & + "[" & $line & ":" & $col & "]") + conf.ideCmd = cmd + 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.clearInstCache(dirtyIdx) + graph.compileProject(dirtyIdx) + 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: + if isKnownFile: + graph.clearInstCache(modIdx) + 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(graph, u) + else: + localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos)) + +proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, graph: ModuleGraph) = + executeNoHooks(cmd, file, dirtyfile, line, col, "", graph) + +proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; tag: string, + graph: ModuleGraph) = + if cmd == ideChk: + graph.config.structuredErrorHook = errorHook + graph.config.writelnHook = myLog + else: + graph.config.structuredErrorHook = nil + graph.config.writelnHook = myLog + executeNoHooks(cmd, file, dirtyfile, line, col, tag, graph) + +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), args[3].getStr, graph) + +proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string, + returnSymbol = "return") = + let response = $convertSexp([newSSymbol(returnSymbol), 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 + of ideProject: echo res.filePath + 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") + of ideProject: stdoutSocket.send(res.filePath & "\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) + of ideProject: + list.add sexp(res.filePath) + 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() + if gAutoBind: + let port = server.connectToNextFreePort(x.address) + server.listen() + echo port + stdout.flushFile() + else: + server.bindAddr(x.port, x.address) + server.listen() + var inp = "" + var stdoutSocket: Socket + while true: + 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.addInt line + result.add ':' + result.addInt col + +proc replEpc(x: ThreadParams) {.thread.} = + var server = newSocket() + let port = connectToNextFreePort(server, "localhost") + server.listen() + echo port + stdout.flushFile() + + var client: Socket + # 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 + of "project": conf.ideCmd = ideProject + of "changed": conf.ideCmd = ideChanged + of "globalsymbols": conf.ideCmd = ideGlobalSymbols + of "declaration": conf.ideCmd = ideDeclaration + of "expand": conf.ideCmd = ideExpand + of "chkfile": conf.ideCmd = ideChkFile + of "recompile": conf.ideCmd = ideRecompile + of "type": conf.ideCmd = ideType + of "inlayhints": + if conf.suggestVersion >= 4: + conf.ideCmd = ideInlayHints + else: + err() + 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) + let tag = substr(cmd, i) + + if conf.ideCmd == ideKnown: + results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig)))) + elif conf.ideCmd == ideProject: + results.send(Suggest(section: ideProject, filePath: string conf.projectFull)) + 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, tag, graph) + sentinel() + +proc recompileFullProject(graph: ModuleGraph) = + benchmark "Recompilation(clean)": + graph.resetForBackend() + graph.resetSystemArtifacts() + graph.vm = nil + graph.resetAllModules() + GC_fullCollect() + graph.compileProject() + +proc mainThread(graph: ModuleGraph) = + let conf = graph.config + myLog "searchPaths: " + for it in conf.searchPaths: + myLog(" " & 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 and conf.suggestVersion < 3: + # 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.setCmd cmdIdeTools + defineSymbol(conf.symbols, $conf.backend) + wantMainModule(conf) + + if not fileExists(conf.projectFull): + quit "cannot find file: " & conf.projectFull.string + + add(conf.searchPaths, conf.libpath) + + conf.setErrorMaxHighMaybe # honor --errorMax even if it may not make sense here + # do not print errors, but log them + conf.writelnHook = proc (msg: string) = discard + + if graph.config.suggestVersion >= 3: + graph.config.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) = + let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info), + line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev) + graph.suggestErrors.mgetOrPut(info.fileIndex, @[]).add suggest + + # compile the project before showing any input so that we already + # can answer questions right away: + benchmark "Initial compilation": + compileProject(graph) + + open(requests) + open(results) + + if graph.config.clientProcessId != 0: + hookProcMonitor(graph.config.clientProcessId) + + 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) + var findProject = false + 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 "v1": conf.suggestVersion = 1 + of "v2": conf.suggestVersion = 0 + of "v3": conf.suggestVersion = 3 + of "v4": conf.suggestVersion = 4 + of "info": + case p.val.normalize + of "protocolver": + stdout.writeLine(HighestSuggestProtocolVersion) + quit 0 + of "nimver": + stdout.writeLine(system.NimVersion) + quit 0 + of "capabilities": + stdout.writeLine(Capabilities.toSeq.mapIt($it).join(" ")) + quit 0 + else: + processSwitch(pass, p, conf) + of "exceptioninlayhints": + case p.val.normalize + of "", "on": incl(conf.globalOptions, optIdeExceptionInlayHints) + of "off": excl(conf.globalOptions, optIdeExceptionInlayHints) + else: processSwitch(pass, p, conf) + 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) + of "find": + findProject = true + of "clientprocessid": + conf.clientProcessId = 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: + if findProject: + conf.projectName = findProjectNimFile(conf, a.parentDir()) + 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 + ) + self.initDefinesProg(conf, "nimsuggest") + + if paramCount() == 0: + stdout.writeLine(Usage) + return + + self.processCmdLineAndProjectPath(conf) + + if gMode != mstdin: + conf.writelnHook = proc (msg: string) = discard + conf.prefixDir = conf.getPrefixDir() + #msgs.writelnHook = proc (line: string) = log(line) + myLog("START " & conf.projectFull.string) + + var graph = newModuleGraph(cache, conf) + if self.loadConfigsAndProcessCmdLine(cache, conf, graph): + + if conf.selectedGC == gcUnselected and + conf.backend != backendJs: + initOrcDefines(conf) + mainCommand(graph) + +# v3 start + +proc recompilePartially(graph: ModuleGraph, projectFileIdx = InvalidFileIdx) = + if projectFileIdx == InvalidFileIdx: + myLog "Recompiling partially from root" + else: + myLog fmt "Recompiling partially starting from {graph.getModule(projectFileIdx)}" + + # inst caches are breaking incremental compilation when the cache caches stuff + # from dirty buffer + graph.clearInstCache(projectFileIdx) + + GC_fullCollect() + + try: + benchmark "Recompilation": + graph.compileProject(projectFileIdx) + except Exception as e: + myLog fmt "Failed to recompile partially with the following error:\n {e.msg} \n\n {e.getStackTrace()}" + try: + graph.recompileFullProject() + except Exception as e: + myLog fmt "Failed clean recompilation:\n {e.msg} \n\n {e.getStackTrace()}" + +func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair] = + # xs contains duplicate items and we want to filter them by range because the + # sym may not match. This can happen when xs contains the same definition but + # with different signature because suggestSym might be called multiple times + # for the same symbol (e. g. including/excluding the pragma) + result = newSeqOfCap[SymInfoPair](xs.len) + for itm in xs.reversed: + var found = false + for res in result: + if res.info.exactEquals(itm.info): + found = true + break + if not found: + result.add(itm) + result.reverse() + +func deduplicateSymInfoPair(xs: SuggestFileSymbolDatabase): SuggestFileSymbolDatabase = + # xs contains duplicate items and we want to filter them by range because the + # sym may not match. This can happen when xs contains the same definition but + # with different signature because suggestSym might be called multiple times + # for the same symbol (e. g. including/excluding the pragma) + result = SuggestFileSymbolDatabase( + lineInfo: newSeqOfCap[TinyLineInfo](xs.lineInfo.len), + sym: newSeqOfCap[PSym](xs.sym.len), + isDecl: newPackedBoolArray(), + caughtExceptions: newSeqOfCap[seq[PType]](xs.caughtExceptions.len), + caughtExceptionsSet: newPackedBoolArray(), + fileIndex: xs.fileIndex, + trackCaughtExceptions: xs.trackCaughtExceptions, + isSorted: false + ) + var i = xs.lineInfo.high + while i >= 0: + let itm = xs.lineInfo[i] + var found = false + for res in result.lineInfo: + if res.exactEquals(itm): + found = true + break + if not found: + result.add(xs.getSymInfoPair(i)) + dec i + result.reverse() + +proc findSymData(graph: ModuleGraph, trackPos: TLineInfo): + ref SymInfoPair = + let db = graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair + doAssert(db.fileIndex == trackPos.fileIndex) + for i in db.lineInfo.low..db.lineInfo.high: + if isTracked(db.lineInfo[i], TinyLineInfo(line: trackPos.line, col: trackPos.col), db.sym[i].name.s.len): + var res = db.getSymInfoPair(i) + new(result) + result[] = res + break + +func isInRange*(current, startPos, endPos: TinyLineInfo, tokenLen: int): bool = + result = + (current.line > startPos.line or (current.line == startPos.line and current.col>=startPos.col)) and + (current.line < endPos.line or (current.line == endPos.line and current.col <= endPos.col)) + +proc findSymDataInRange(graph: ModuleGraph, startPos, endPos: TLineInfo): + seq[SymInfoPair] = + result = newSeq[SymInfoPair]() + let db = graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair + for i in db.lineInfo.low..db.lineInfo.high: + if isInRange(db.lineInfo[i], TinyLineInfo(line: startPos.line, col: startPos.col), TinyLineInfo(line: endPos.line, col: endPos.col), db.sym[i].name.s.len): + result.add(db.getSymInfoPair(i)) + +proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int): + ref SymInfoPair = + let + fileIdx = fileInfoIdx(graph.config, file) + trackPos = newLineInfo(fileIdx, line, col) + result = findSymData(graph, trackPos) + +proc findSymDataInRange(graph: ModuleGraph, file: AbsoluteFile; startLine, startCol, endLine, endCol: int): + seq[SymInfoPair] = + let + fileIdx = fileInfoIdx(graph.config, file) + startPos = newLineInfo(fileIdx, startLine, startCol) + endPos = newLineInfo(fileIdx, endLine, endCol) + result = findSymDataInRange(graph, startPos, endPos) + +proc markDirtyIfNeeded(graph: ModuleGraph, file: string, originalFileIdx: FileIndex) = + let sha = $sha1.secureHashFile(file) + if graph.config.m.fileInfos[originalFileIdx.int32].hash != sha or graph.config.ideCmd in {ideSug, ideCon}: + myLog fmt "{file} changed compared to last compilation" + graph.markDirty originalFileIdx + graph.markClientsDirty originalFileIdx + else: + myLog fmt "No changes in file {file} compared to last compilation" + +proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) = + let section = if defaultSection != ideNone: + defaultSection + elif sym.info.exactEquals(info): + ideDef + else: + ideUse + let suggest = symToSuggest(graph, sym, isLocal=false, section, + info, 100, PrefixMatch.None, false, 0, + endLine = endLine, endCol = endCol) + suggestResult(graph.config, suggest) + +proc suggestInlayHintResultType(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) = + let section = if defaultSection != ideNone: + defaultSection + elif sym.info.exactEquals(info): + ideDef + else: + ideUse + var suggestDef = symToSuggest(graph, sym, isLocal=false, section, + info, 100, PrefixMatch.None, false, 0, true, + endLine = endLine, endCol = endCol) + suggestDef.inlayHintInfo = suggestToSuggestInlayTypeHint(suggestDef) + suggestDef.section = ideInlayHints + if sym.kind == skForVar: + suggestDef.inlayHintInfo.allowInsert = false + suggestResult(graph.config, suggestDef) + +proc suggestInlayHintResultException(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, caughtExceptions: seq[PType], caughtExceptionsSet: bool, endLine: uint16 = 0, endCol = 0) = + if not caughtExceptionsSet: + return + + if sym.kind == skParam and sfEffectsDelayed in sym.flags: + return + + var raisesList: seq[PType] = @[getEbase(graph, info)] + + let t = sym.typ + if not isNil(t) and not isNil(t.n) and t.n.len > 0 and t.n[0].len > exceptionEffects: + let effects = t.n[0] + if effects.kind == nkEffectList and effects.len == effectListLen: + let effs = effects[exceptionEffects] + if not isNil(effs): + raisesList = @[] + for eff in items(effs): + if not isNil(eff): + raisesList.add(eff.typ) + + var propagatedExceptionList: seq[PType] = @[] + for re in raisesList: + var exceptionIsPropagated = true + for ce in caughtExceptions: + if isNil(ce) or safeInheritanceDiff(re, ce) <= 0: + exceptionIsPropagated = false + break + if exceptionIsPropagated: + propagatedExceptionList.add(re) + + if propagatedExceptionList.len == 0: + return + + let section = if defaultSection != ideNone: + defaultSection + elif sym.info.exactEquals(info): + ideDef + else: + ideUse + var suggestDef = symToSuggest(graph, sym, isLocal=false, section, + info, 100, PrefixMatch.None, false, 0, true, + endLine = endLine, endCol = endCol) + suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintLeft(suggestDef, propagatedExceptionList) + suggestDef.section = ideInlayHints + suggestResult(graph.config, suggestDef) + suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintRight(suggestDef, propagatedExceptionList) + suggestResult(graph.config, suggestDef) + +const + # kinds for ideOutline and ideGlobalSymbols + searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate} + +proc symbolEqual(left, right: PSym): bool = + # More relaxed symbol comparison + return left.info.exactEquals(right.info) and left.name == right.name + +proc findDef(n: PNode, line: uint16, col: int16): PNode = + if n.kind in {nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkMacroDef}: + if n.info.line == line: + return n + else: + for i in 0 ..< safeLen(n): + let res = findDef(n[i], line, col) + if res != nil: return res + +proc findByTLineInfo(trackPos: TLineInfo, infoPairs: SuggestFileSymbolDatabase): + ref SymInfoPair = + result = nil + if infoPairs.fileIndex == trackPos.fileIndex: + for i in infoPairs.lineInfo.low..infoPairs.lineInfo.high: + let s = infoPairs.getSymInfoPair(i) + if s.info.exactEquals trackPos: + new(result) + result[] = s + break + +proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool = + proc checkSymbol(sym: PSym, info: TLineInfo): bool = + result = (sym.owner.kind in {skModule, skType} or sym.kind in {skProc, skMethod, skIterator, skTemplate, skType}) + + if n.kind == nkSym and n.sym.checkSymbol(n.info): + graph.suggestResult(n.sym, n.sym.info, ideOutline, endInfo.line, endInfo.col) + return true + elif n.kind == nkIdent: + let symData = findByTLineInfo(n.info, infoPairs) + if symData != nil and symData.sym.checkSymbol(symData.info): + let sym = symData.sym + graph.suggestResult(sym, sym.info, ideOutline, endInfo.line, endInfo.col) + return true + +proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool = + for child in n: + if child.kind in {nkIdent, nkSym}: + if graph.outlineNode(child, endInfo, infoPairs): + return true + elif child.kind == nkPostfix: + if graph.handleIdentOrSym(child, endInfo, infoPairs): + return true + +proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: SuggestFileSymbolDatabase) = + var matched = true + if n.kind == nkIdent: + let symData = findByTLineInfo(n.info, infoPairs) + if symData != nil and symData.sym.kind == skEnumField and symData.info.exactEquals(symData.sym.info): + let sym = symData.sym + graph.suggestResult(sym, sym.info, ideOutline, n.endInfo.line, n.endInfo.col) + elif (n.kind in {nkFuncDef, nkProcDef, nkTypeDef, nkMacroDef, nkTemplateDef, nkConverterDef, nkEnumFieldDef, nkConstDef}): + matched = handleIdentOrSym(graph, n, n.endInfo, infoPairs) + else: + matched = false + + if n.kind != nkFormalParams: + for child in n: + graph.iterateOutlineNodes(child, infoPairs) + +proc calculateExpandRange(n: PNode, info: TLineInfo): TLineInfo = + if ((n.kind in {nkFuncDef, nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkConverterDef} and + n.info.exactEquals(info)) or + (n.kind in {nkCall, nkCommand} and n[0].info.exactEquals(info))): + result = n.endInfo + else: + for child in n: + result = child.calculateExpandRange(info) + if result != unknownLineInfo: + return result + result = unknownLineInfo + +proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string, + graph: ModuleGraph) = + let conf = graph.config + conf.writelnHook = proc (s: string) = discard + conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; + msg: string; sev: Severity) = + let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info), + line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev) + graph.suggestErrors.mgetOrPut(info.fileIndex, @[]).add suggest + + conf.ideCmd = cmd + + myLog fmt "cmd: {cmd}, file: {file}[{line}:{col}], dirtyFile: {dirtyfile}, tag: {tag}" + + var fileIndex: FileIndex + + if not (cmd in {ideRecompile, ideGlobalSymbols}): + fileIndex = fileInfoIdx(conf, file) + msgs.setDirtyFile( + conf, + fileIndex, + if dirtyfile.isEmpty: AbsoluteFile"" else: dirtyfile) + + if not dirtyfile.isEmpty: + graph.markDirtyIfNeeded(dirtyFile.string, fileInfoIdx(conf, file)) + + # these commands require fully compiled project + if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk, ideInlayHints} and graph.needsCompilation(): + graph.recompilePartially() + # when doing incremental build for the project root we should make sure that + # everything is unmarked as no longer beeing dirty in case there is no + # longer reference to a particular module. E. g. A depends on B, B is marked + # as dirty and A loses B import. + graph.unmarkAllDirty() + + # these commands require partially compiled project + elif cmd in {ideSug, ideCon, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration, ideExpand} and + (graph.needsCompilation(fileIndex) or cmd in {ideSug, ideCon}): + # for ideSug use v2 implementation + if cmd in {ideSug, ideCon}: + conf.m.trackPos = newLineInfo(fileIndex, line, col) + conf.m.trackPosAttached = false + else: + conf.m.trackPos = default(TLineInfo) + graph.recompilePartially(fileIndex) + + case cmd + of ideDef: + let s = graph.findSymData(file, line, col) + if not s.isNil: + graph.suggestResult(s.sym, s.sym.info) + of ideType: + let s = graph.findSymData(file, line, col) + if not s.isNil: + let typeSym = s.sym.typ.sym + if typeSym != nil: + graph.suggestResult(typeSym, typeSym.info, ideType) + elif s.sym.typ.len != 0: + let genericType = s.sym.typ[0].sym + graph.suggestResult(genericType, genericType.info, ideType) + of ideUse, ideDus: + let symbol = graph.findSymData(file, line, col) + if not symbol.isNil: + var res: seq[SymInfoPair] = @[] + for s in graph.suggestSymbolsIter: + if s.sym.symbolEqual(symbol.sym): + res.add(s) + for s in res.deduplicateSymInfoPair(): + graph.suggestResult(s.sym, s.info) + of ideHighlight: + let sym = graph.findSymData(file, line, col) + if not sym.isNil: + let fs = graph.fileSymbols(fileIndex) + var usages: seq[SymInfoPair] = @[] + for i in fs.lineInfo.low..fs.lineInfo.high: + if fs.sym[i] == sym.sym: + usages.add(fs.getSymInfoPair(i)) + myLog fmt "Found {usages.len} usages in {file.string}" + for s in usages: + graph.suggestResult(s.sym, s.info) + of ideRecompile: + graph.recompileFullProject() + of ideChanged: + graph.markDirtyIfNeeded(file.string, fileIndex) + of ideSug, ideCon: + # ideSug/ideCon performs partial build of the file, thus mark it dirty for the + # future calls. + graph.markDirtyIfNeeded(file.string, fileIndex) + graph.recompilePartially(fileIndex) + let m = graph.getModule fileIndex + incl m.flags, sfDirty + of ideOutline: + let n = parseFile(fileIndex, graph.cache, graph.config) + graph.iterateOutlineNodes(n, graph.fileSymbols(fileIndex).deduplicateSymInfoPair) + of ideChk: + myLog fmt "Reporting errors for {graph.suggestErrors.len} file(s)" + for sug in graph.suggestErrorsIter: + suggestResult(graph.config, sug) + of ideChkFile: + let errors = graph.suggestErrors.getOrDefault(fileIndex, @[]) + myLog fmt "Reporting {errors.len} error(s) for {file.string}" + for error in errors: + suggestResult(graph.config, error) + of ideGlobalSymbols: + var + counter = 0 + res: seq[SymInfoPair] = @[] + + for s in graph.suggestSymbolsIter: + if (sfGlobal in s.sym.flags or s.sym.kind in searchableSymKinds) and + s.sym.info == s.info: + if contains(s.sym.name.s, file.string): + inc counter + res = res.filterIt(not it.info.exactEquals(s.info)) + res.add s + # stop after first 1000 matches... + if counter > 1000: + break + + # ... then sort them by weight ... + res.sort() do (left, right: SymInfoPair) -> int: + let + leftString = left.sym.name.s + rightString = right.sym.name.s + leftIndex = leftString.find(file.string) + rightIndex = rightString.find(file.string) + + if leftIndex == rightIndex: + result = cmp(toLowerAscii(leftString), + toLowerAscii(rightString)) + else: + result = cmp(leftIndex, rightIndex) + + # ... and send first 100 results + if res.len > 0: + for i in 0 .. min(100, res.len - 1): + let s = res[i] + graph.suggestResult(s.sym, s.info) + + of ideDeclaration: + let s = graph.findSymData(file, line, col) + if not s.isNil: + # find first mention of the symbol in the file containing the definition. + # It is either the definition or the declaration. + var first: SymInfoPair + let db = graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair + for i in db.lineInfo.low..db.lineInfo.high: + if s.sym.symbolEqual(db.sym[i]): + first = db.getSymInfoPair(i) + break + + if s.info.exactEquals(first.info): + # we are on declaration, go to definition + graph.suggestResult(first.sym, first.sym.info, ideDeclaration) + else: + # we are on definition or usage, look for declaration + graph.suggestResult(first.sym, first.info, ideDeclaration) + of ideExpand: + var level: int = high(int) + let index = skipWhitespace(tag, 0); + let trimmed = substr(tag, index) + if not (trimmed == "" or trimmed == "all"): + discard parseInt(trimmed, level, 0) + + conf.expandPosition = newLineInfo(fileIndex, line, col) + conf.expandLevels = level + conf.expandProgress = false + conf.expandNodeResult = "" + + graph.markDirty fileIndex + graph.markClientsDirty fileIndex + graph.recompilePartially() + var suggest = Suggest() + suggest.section = ideExpand + suggest.version = 3 + suggest.line = line + suggest.column = col + suggest.doc = graph.config.expandNodeResult + if suggest.doc != "": + let + n = parseFile(fileIndex, graph.cache, graph.config) + endInfo = n.calculateExpandRange(conf.expandPosition) + + suggest.endLine = endInfo.line + suggest.endCol = endInfo.col + + suggestResult(graph.config, suggest) + + graph.markDirty fileIndex + graph.markClientsDirty fileIndex + of ideInlayHints: + myLog fmt "Executing inlayHints" + var endLine = 0 + var endCol = -1 + var i = 0 + i += skipWhile(tag, seps, i) + i += parseInt(tag, endLine, i) + i += skipWhile(tag, seps, i) + i += parseInt(tag, endCol, i) + i += skipWhile(tag, seps, i) + var typeHints = true + var exceptionHints = false + while i <= tag.high: + var token: string + i += parseUntil(tag, token, seps, i) + i += skipWhile(tag, seps, i) + case token: + of "+typeHints": + typeHints = true + of "-typeHints": + typeHints = false + of "+exceptionHints": + exceptionHints = true + of "-exceptionHints": + exceptionHints = false + else: + myLog fmt "Discarding unknown inlay hint parameter {token}" + + let s = graph.findSymDataInRange(file, line, col, endLine, endCol) + for q in s: + if typeHints and q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType: + graph.suggestInlayHintResultType(q.sym, q.info, ideInlayHints) + if exceptionHints and q.sym.kind in {skProc, skFunc, skMethod, skVar, skLet, skParam} and not q.isDecl: + graph.suggestInlayHintResultException(q.sym, q.info, ideInlayHints, caughtExceptions = q.caughtExceptions, caughtExceptionsSet = q.caughtExceptionsSet) + else: + myLog fmt "Discarding {cmd}" + +# v3 end +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 + conf.setCmd cmdIdeTools + defineSymbol(conf.symbols, $conf.backend) + clearPasses(graph) + registerPass graph, verbosePass + registerPass graph, semPass + + wantMainModule(conf) + + if not fileExists(conf.projectFull): + quit "cannot find file: " & conf.projectFull.string + + add(conf.searchPaths, conf.libpath) + + conf.setErrorMaxHighMaybe + # 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 + ) + self.initDefinesProg(conf, "nimsuggest") + + self.processCmdLineAndProjectPath(conf) + + if gMode != mstdin: + conf.writelnHook = proc (msg: string) = discard + # Find Nim's prefix dir. + if nimPath == "": + conf.prefixDir = conf.getPrefixDir() + else: + conf.prefixDir = AbsoluteDir nimPath + + #msgs.writelnHook = proc (line: string) = log(line) + myLog("START " & conf.projectFull.string) + + var graph = newModuleGraph(cache, conf) + if self.loadConfigsAndProcessCmdLine(cache, conf, graph): + mockCommand(graph) + if gLogging: + myLog("Search paths:") + for it in conf.searchPaths: + myLog(" " & 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)))) + elif conf.ideCmd == ideProject: + retval.add(Suggest(section: ideProject, filePath: string conf.projectFull)) + 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 |