summary refs log tree commit diff stats
path: root/nimsuggest/nimsuggest.nim
diff options
context:
space:
mode:
Diffstat (limited to 'nimsuggest/nimsuggest.nim')
-rw-r--r--nimsuggest/nimsuggest.nim1392
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