1#
2#
3#           The Nim Compiler
4#        (c) Copyright 2017 Andreas Rumpf
5#
6#    See the file "copying.txt", included in this
7#    distribution, for details about the copyright.
8#
9
10## Nimsuggest is a tool that helps to give editors IDE like capabilities.
11
12when not defined(nimcore):
13  {.error: "nimcore MUST be defined for Nim's core tooling".}
14
15import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp
16# Do NOT import suggest. It will lead to weird bugs with
17# suggestionResultHook, because suggest.nim is included by sigmatch.
18# So we import that one instead.
19import compiler / [options, commands, modules, sem,
20  passes, passaux, msgs,
21  sigmatch, ast,
22  idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper,
23  pathutils]
24
25when defined(windows):
26  import winlean
27else:
28  import posix
29
30const DummyEof = "!EOF!"
31const Usage = """
32Nimsuggest - Tool to give every editor IDE like capabilities for Nim
33Usage:
34  nimsuggest [options] projectfile.nim
35
36Options:
37  --autobind              automatically binds into a free port
38  --port:PORT             port, by default 6000
39  --address:HOST          binds to that address, by default ""
40  --stdin                 read commands from stdin and write results to
41                          stdout instead of using sockets
42  --epc                   use emacs epc mode
43  --debug                 enable debug output
44  --log                   enable verbose logging to nimsuggest.log file
45  --v1                    use version 1 of the protocol; for backwards compatibility
46  --refresh               perform automatic refreshes to keep the analysis precise
47  --maxresults:N          limit the number of suggestions to N
48  --tester                implies --stdin and outputs a line
49                          '""" & DummyEof & """' for the tester
50  --find                  attempts to find the project file of the current project
51
52The server then listens to the connection and takes line-based commands.
53
54If --autobind is used, the binded port number will be printed to stdout.
55
56In addition, all command line options of Nim that do not affect code generation
57are supported.
58"""
59type
60  Mode = enum mstdin, mtcp, mepc, mcmdsug, mcmdcon
61  CachedMsg = object
62    info: TLineInfo
63    msg: string
64    sev: Severity
65  CachedMsgs = seq[CachedMsg]
66
67var
68  gPort = 6000.Port
69  gAddress = ""
70  gMode: Mode
71  gEmitEof: bool # whether we write '!EOF!' dummy lines
72  gLogging = defined(logging)
73  gRefresh: bool
74  gAutoBind = false
75
76  requests: Channel[string]
77  results: Channel[Suggest]
78
79proc writelnToChannel(line: string) =
80  results.send(Suggest(section: ideMsg, doc: line))
81
82proc sugResultHook(s: Suggest) =
83  results.send(s)
84
85proc errorHook(conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
86  results.send(Suggest(section: ideChk, filePath: toFullPath(conf, info),
87    line: toLinenumber(info), column: toColumn(info), doc: msg,
88    forth: $sev))
89
90proc myLog(s: string) =
91  if gLogging: log(s)
92
93const
94  seps = {':', ';', ' ', '\t'}
95  Help = "usage: sug|con|def|use|dus|chk|mod|highlight|outline|known|project file.nim[;dirtyfile.nim]:line:col\n" &
96         "type 'quit' to quit\n" &
97         "type 'debug' to toggle debug mode on/off\n" &
98         "type 'terse' to toggle terse mode on/off"
99
100proc parseQuoted(cmd: string; outp: var string; start: int): int =
101  var i = start
102  i += skipWhitespace(cmd, i)
103  if i < cmd.len and cmd[i] == '"':
104    i += parseUntil(cmd, outp, '"', i+1)+2
105  else:
106    i += parseUntil(cmd, outp, seps, i)
107  result = i
108
109proc sexp(s: IdeCmd|TSymKind|PrefixMatch): SexpNode = sexp($s)
110
111proc sexp(s: Suggest): SexpNode =
112  # If you change the order here, make sure to change it over in
113  # nim-mode.el too.
114  let qp = if s.qualifiedPath.len == 0: @[] else: s.qualifiedPath
115  result = convertSexp([
116    s.section,
117    TSymKind s.symkind,
118    qp.map(newSString),
119    s.filePath,
120    s.forth,
121    s.line,
122    s.column,
123    s.doc,
124    s.quality
125  ])
126  if s.section == ideSug:
127    result.add convertSexp(s.prefix)
128
129proc sexp(s: seq[Suggest]): SexpNode =
130  result = newSList()
131  for sug in s:
132    result.add(sexp(sug))
133
134proc listEpc(): SexpNode =
135  # This function is called from Emacs to show available options.
136  let
137    argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol))
138    docstring = sexp("line starts at 1, column at 0, dirtyfile is optional")
139  result = newSList()
140  for command in ["sug", "con", "def", "use", "dus", "chk", "mod"]:
141    let
142      cmd = sexp(command)
143      methodDesc = newSList()
144    methodDesc.add(cmd)
145    methodDesc.add(argspecs)
146    methodDesc.add(docstring)
147    result.add(methodDesc)
148
149proc findNode(n: PNode; trackPos: TLineInfo): PSym =
150  #echo "checking node ", n.info
151  if n.kind == nkSym:
152    if isTracked(n.info, trackPos, n.sym.name.s.len): return n.sym
153  else:
154    for i in 0 ..< safeLen(n):
155      let res = findNode(n[i], trackPos)
156      if res != nil: return res
157
158proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo): PSym =
159  let m = graph.getModule(trackPos.fileIndex)
160  if m != nil and m.ast != nil:
161    result = findNode(m.ast, trackPos)
162
163proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int;
164             graph: ModuleGraph) =
165  let conf = graph.config
166  myLog("cmd: " & $cmd & ", file: " & file.string &
167        ", dirtyFile: " & dirtyfile.string &
168        "[" & $line & ":" & $col & "]")
169  conf.ideCmd = cmd
170  if cmd == ideUse and conf.suggestVersion != 0:
171    graph.resetAllModules()
172  var isKnownFile = true
173  let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
174
175  if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile)
176  else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"")
177
178  conf.m.trackPos = newLineInfo(dirtyIdx, line, col)
179  conf.m.trackPosAttached = false
180  conf.errorCounter = 0
181  if conf.suggestVersion == 1:
182    graph.usageSym = nil
183  if not isKnownFile:
184    graph.compileProject(dirtyIdx)
185  if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and
186      dirtyfile.isEmpty:
187    discard "no need to recompile anything"
188  else:
189    let modIdx = graph.parentModule(dirtyIdx)
190    graph.markDirty dirtyIdx
191    graph.markClientsDirty dirtyIdx
192    if conf.ideCmd != ideMod:
193      if isKnownFile:
194        graph.compileProject(modIdx)
195  if conf.ideCmd in {ideUse, ideDus}:
196    let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym
197    if u != nil:
198      listUsages(graph, u)
199    else:
200      localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos))
201
202proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int;
203             graph: ModuleGraph) =
204  if cmd == ideChk:
205    graph.config.structuredErrorHook = errorHook
206    graph.config.writelnHook = myLog
207  else:
208    graph.config.structuredErrorHook = nil
209    graph.config.writelnHook = myLog
210  executeNoHooks(cmd, file, dirtyfile, line, col, graph)
211
212proc executeEpc(cmd: IdeCmd, args: SexpNode;
213                graph: ModuleGraph) =
214  let
215    file = AbsoluteFile args[0].getStr
216    line = args[1].getNum
217    column = args[2].getNum
218  var dirtyfile = AbsoluteFile""
219  if len(args) > 3:
220    dirtyfile = AbsoluteFile args[3].getStr("")
221  execute(cmd, file, dirtyfile, int(line), int(column), graph)
222
223proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string,
224               returnSymbol = "return") =
225  let response = $convertSexp([newSSymbol(returnSymbol), uid, s])
226  socket.send(toHex(len(response), 6))
227  socket.send(response)
228
229template checkSanity(client, sizeHex, size, messageBuffer: typed) =
230  if client.recv(sizeHex, 6) != 6:
231    raise newException(ValueError, "didn't get all the hexbytes")
232  if parseHex(sizeHex, size) == 0:
233    raise newException(ValueError, "invalid size hex: " & $sizeHex)
234  if client.recv(messageBuffer, size) != size:
235    raise newException(ValueError, "didn't get all the bytes")
236
237proc toStdout() {.gcsafe.} =
238  while true:
239    let res = results.recv()
240    case res.section
241    of ideNone: break
242    of ideMsg: echo res.doc
243    of ideKnown: echo res.quality == 1
244    of ideProject: echo res.filePath
245    else: echo res
246
247proc toSocket(stdoutSocket: Socket) {.gcsafe.} =
248  while true:
249    let res = results.recv()
250    case res.section
251    of ideNone: break
252    of ideMsg: stdoutSocket.send(res.doc & "\c\L")
253    of ideKnown: stdoutSocket.send($(res.quality == 1) & "\c\L")
254    of ideProject: stdoutSocket.send(res.filePath & "\c\L")
255    else: stdoutSocket.send($res & "\c\L")
256
257proc toEpc(client: Socket; uid: BiggestInt) {.gcsafe.} =
258  var list = newSList()
259  while true:
260    let res = results.recv()
261    case res.section
262    of ideNone: break
263    of ideMsg:
264      list.add sexp(res.doc)
265    of ideKnown:
266      list.add sexp(res.quality == 1)
267    of ideProject:
268      list.add sexp(res.filePath)
269    else:
270      list.add sexp(res)
271  returnEpc(client, uid, list)
272
273template setVerbosity(level: typed) =
274  gVerbosity = level
275  conf.notes = NotesVerbosity[gVerbosity]
276
277proc connectToNextFreePort(server: Socket, host: string): Port =
278  server.bindAddr(Port(0), host)
279  let (_, port) = server.getLocalAddr
280  result = port
281
282type
283  ThreadParams = tuple[port: Port; address: string]
284
285proc replStdinSingleCmd(line: string) =
286  requests.send line
287  toStdout()
288  echo ""
289  flushFile(stdout)
290
291proc replStdin(x: ThreadParams) {.thread.} =
292  if gEmitEof:
293    echo DummyEof
294    while true:
295      let line = readLine(stdin)
296      requests.send line
297      if line == "quit": break
298      toStdout()
299      echo DummyEof
300      flushFile(stdout)
301  else:
302    echo Help
303    var line = ""
304    while readLineFromStdin("> ", line):
305      replStdinSingleCmd(line)
306    requests.send "quit"
307
308proc replCmdline(x: ThreadParams) {.thread.} =
309  replStdinSingleCmd(x.address)
310  requests.send "quit"
311
312proc replTcp(x: ThreadParams) {.thread.} =
313  var server = newSocket()
314  if gAutoBind:
315    let port = server.connectToNextFreePort(x.address)
316    server.listen()
317    echo port
318    stdout.flushFile()
319  else:
320    server.bindAddr(x.port, x.address)
321    server.listen()
322  var inp = ""
323  var stdoutSocket: Socket
324  while true:
325    accept(server, stdoutSocket)
326
327    stdoutSocket.readLine(inp)
328    requests.send inp
329    toSocket(stdoutSocket)
330    stdoutSocket.send("\c\L")
331    stdoutSocket.close()
332
333proc argsToStr(x: SexpNode): string =
334  if x.kind != SList: return x.getStr
335  doAssert x.kind == SList
336  doAssert x.len >= 4
337  let file = x[0].getStr
338  let line = x[1].getNum
339  let col = x[2].getNum
340  let dirty = x[3].getStr
341  result = x[0].getStr.escape
342  if dirty.len > 0:
343    result.add ';'
344    result.add dirty.escape
345  result.add ':'
346  result.addInt line
347  result.add ':'
348  result.addInt col
349
350proc replEpc(x: ThreadParams) {.thread.} =
351  var server = newSocket()
352  let port = connectToNextFreePort(server, "localhost")
353  server.listen()
354  echo port
355  stdout.flushFile()
356
357  var client: Socket
358  # Wait for connection
359  accept(server, client)
360  while true:
361    var
362      sizeHex = ""
363      size = 0
364      messageBuffer = ""
365    checkSanity(client, sizeHex, size, messageBuffer)
366    let
367      message = parseSexp($messageBuffer)
368      epcApi = message[0].getSymbol
369    case epcApi
370    of "call":
371      let
372        uid = message[1].getNum
373        cmd = message[2].getSymbol
374        args = message[3]
375
376      when false:
377        x.ideCmd[] = parseIdeCmd(message[2].getSymbol)
378        case x.ideCmd[]
379        of ideSug, ideCon, ideDef, ideUse, ideDus, ideOutline, ideHighlight:
380          setVerbosity(0)
381        else: discard
382      let fullCmd = cmd & " " & args.argsToStr
383      myLog "MSG CMD: " & fullCmd
384      requests.send(fullCmd)
385      toEpc(client, uid)
386    of "methods":
387      returnEpc(client, message[1].getNum, listEpc())
388    of "epc-error":
389      # an unhandled exception forces down the whole process anyway, so we
390      # use 'quit' here instead of 'raise'
391      quit("received epc error: " & $messageBuffer)
392    else:
393      let errMessage = case epcApi
394                       of "return", "return-error":
395                         "no return expected"
396                       else:
397                         "unexpected call: " & epcApi
398      quit errMessage
399
400proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) =
401  let conf = graph.config
402
403  template sentinel() =
404    # send sentinel for the input reading thread:
405    results.send(Suggest(section: ideNone))
406
407  template toggle(sw) =
408    if sw in conf.globalOptions:
409      excl(conf.globalOptions, sw)
410    else:
411      incl(conf.globalOptions, sw)
412    sentinel()
413    return
414
415  template err() =
416    echo Help
417    sentinel()
418    return
419
420  var opc = ""
421  var i = parseIdent(cmd, opc, 0)
422  case opc.normalize
423  of "sug": conf.ideCmd = ideSug
424  of "con": conf.ideCmd = ideCon
425  of "def": conf.ideCmd = ideDef
426  of "use": conf.ideCmd = ideUse
427  of "dus": conf.ideCmd = ideDus
428  of "mod": conf.ideCmd = ideMod
429  of "chk": conf.ideCmd = ideChk
430  of "highlight": conf.ideCmd = ideHighlight
431  of "outline": conf.ideCmd = ideOutline
432  of "quit":
433    sentinel()
434    quit()
435  of "debug": toggle optIdeDebug
436  of "terse": toggle optIdeTerse
437  of "known": conf.ideCmd = ideKnown
438  of "project": conf.ideCmd = ideProject
439  else: err()
440  var dirtyfile = ""
441  var orig = ""
442  i += skipWhitespace(cmd, i)
443  if i < cmd.len and cmd[i] in {'0'..'9'}:
444    orig = string conf.projectFull
445  else:
446    i = parseQuoted(cmd, orig, i)
447    if i < cmd.len and cmd[i] == ';':
448      i = parseQuoted(cmd, dirtyfile, i+1)
449    i += skipWhile(cmd, seps, i)
450  var line = 0
451  var col = -1
452  i += parseInt(cmd, line, i)
453  i += skipWhile(cmd, seps, i)
454  i += parseInt(cmd, col, i)
455
456  if conf.ideCmd == ideKnown:
457    results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig))))
458  elif conf.ideCmd == ideProject:
459    results.send(Suggest(section: ideProject, filePath: string conf.projectFull))
460  else:
461    if conf.ideCmd == ideChk:
462      for cm in cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
463    execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, graph)
464  sentinel()
465
466proc recompileFullProject(graph: ModuleGraph) =
467  #echo "recompiling full project"
468  resetSystemArtifacts(graph)
469  graph.vm = nil
470  graph.resetAllModules()
471  GC_fullCollect()
472  compileProject(graph)
473  #echo GC_getStatistics()
474
475proc mainThread(graph: ModuleGraph) =
476  let conf = graph.config
477  if gLogging:
478    for it in conf.searchPaths:
479      log(it.string)
480
481  proc wrHook(line: string) {.closure.} =
482    if gMode == mepc:
483      if gLogging: log(line)
484    else:
485      writelnToChannel(line)
486
487  conf.writelnHook = wrHook
488  conf.suggestionResultHook = sugResultHook
489  graph.doStopCompile = proc (): bool = requests.peek() > 0
490  var idle = 0
491  var cachedMsgs: CachedMsgs = @[]
492  while true:
493    let (hasData, req) = requests.tryRecv()
494    if hasData:
495      conf.writelnHook = wrHook
496      conf.suggestionResultHook = sugResultHook
497      execCmd(req, graph, cachedMsgs)
498      idle = 0
499    else:
500      os.sleep 250
501      idle += 1
502    if idle == 20 and gRefresh:
503      # we use some nimsuggest activity to enable a lazy recompile:
504      conf.ideCmd = ideChk
505      conf.writelnHook = proc (s: string) = discard
506      cachedMsgs.setLen 0
507      conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
508        cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev))
509      conf.suggestionResultHook = proc (s: Suggest) = discard
510      recompileFullProject(graph)
511
512var
513  inputThread: Thread[ThreadParams]
514
515proc mainCommand(graph: ModuleGraph) =
516  let conf = graph.config
517  clearPasses(graph)
518  registerPass graph, verbosePass
519  registerPass graph, semPass
520  conf.setCmd cmdIdeTools
521  wantMainModule(conf)
522
523  if not fileExists(conf.projectFull):
524    quit "cannot find file: " & conf.projectFull.string
525
526  add(conf.searchPaths, conf.libpath)
527
528  conf.setErrorMaxHighMaybe # honor --errorMax even if it may not make sense here
529  # do not print errors, but log them
530  conf.writelnHook = myLog
531  conf.structuredErrorHook = nil
532
533  # compile the project before showing any input so that we already
534  # can answer questions right away:
535  compileProject(graph)
536
537  open(requests)
538  open(results)
539
540  case gMode
541  of mstdin: createThread(inputThread, replStdin, (gPort, gAddress))
542  of mtcp: createThread(inputThread, replTcp, (gPort, gAddress))
543  of mepc: createThread(inputThread, replEpc, (gPort, gAddress))
544  of mcmdsug: createThread(inputThread, replCmdline,
545                            (gPort, "sug \"" & conf.projectFull.string & "\":" & gAddress))
546  of mcmdcon: createThread(inputThread, replCmdline,
547                            (gPort, "con \"" & conf.projectFull.string & "\":" & gAddress))
548  mainThread(graph)
549  joinThread(inputThread)
550  close(requests)
551  close(results)
552
553proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
554  var p = parseopt.initOptParser(cmd)
555  var findProject = false
556  while true:
557    parseopt.next(p)
558    case p.kind
559    of cmdEnd: break
560    of cmdLongOption, cmdShortOption:
561      case p.key.normalize
562      of "help", "h":
563        stdout.writeLine(Usage)
564        quit()
565      of "autobind":
566        gMode = mtcp
567        gAutoBind = true
568      of "port":
569        gPort = parseInt(p.val).Port
570        gMode = mtcp
571      of "address":
572        gAddress = p.val
573        gMode = mtcp
574      of "stdin": gMode = mstdin
575      of "cmdsug":
576        gMode = mcmdsug
577        gAddress = p.val
578        incl(conf.globalOptions, optIdeDebug)
579      of "cmdcon":
580        gMode = mcmdcon
581        gAddress = p.val
582        incl(conf.globalOptions, optIdeDebug)
583      of "epc":
584        gMode = mepc
585        conf.verbosity = 0          # Port number gotta be first.
586      of "debug": incl(conf.globalOptions, optIdeDebug)
587      of "v2": conf.suggestVersion = 0
588      of "v1": conf.suggestVersion = 1
589      of "tester":
590        gMode = mstdin
591        gEmitEof = true
592        gRefresh = false
593      of "log": gLogging = true
594      of "refresh":
595        if p.val.len > 0:
596          gRefresh = parseBool(p.val)
597        else:
598          gRefresh = true
599      of "maxresults":
600        conf.suggestMaxResults = parseInt(p.val)
601      of "find":
602        findProject = true
603      else: processSwitch(pass, p, conf)
604    of cmdArgument:
605      let a = unixToNativePath(p.key)
606      if dirExists(a) and not fileExists(a.addFileExt("nim")):
607        conf.projectName = findProjectNimFile(conf, a)
608        # don't make it worse, report the error the old way:
609        if conf.projectName.len == 0: conf.projectName = a
610      else:
611        if findProject:
612          conf.projectName = findProjectNimFile(conf, a.parentDir())
613          if conf.projectName.len == 0:
614            conf.projectName = a
615        else:
616          conf.projectName = a
617      # if processArgument(pass, p, argsCount): break
618
619proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
620  let self = NimProg(
621    suggestMode: true,
622    processCmdLine: processCmdLine
623  )
624  self.initDefinesProg(conf, "nimsuggest")
625
626  if paramCount() == 0:
627    stdout.writeLine(Usage)
628    return
629
630  self.processCmdLineAndProjectPath(conf)
631
632  if gMode != mstdin:
633    conf.writelnHook = proc (msg: string) = discard
634  # Find Nim's prefix dir.
635  let binaryPath = findExe("nim")
636  if binaryPath == "":
637    raise newException(IOError,
638        "Cannot find Nim standard library: Nim compiler not in PATH")
639  conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
640  if not dirExists(conf.prefixDir / RelativeDir"lib"):
641    conf.prefixDir = AbsoluteDir""
642
643  #msgs.writelnHook = proc (line: string) = log(line)
644  myLog("START " & conf.projectFull.string)
645
646  var graph = newModuleGraph(cache, conf)
647  if self.loadConfigsAndProcessCmdLine(cache, conf, graph):
648    mainCommand(graph)
649
650when isMainModule:
651  handleCmdLine(newIdentCache(), newConfigRef())
652else:
653  export Suggest
654  export IdeCmd
655  export AbsoluteFile
656  type NimSuggest* = ref object
657    graph: ModuleGraph
658    idle: int
659    cachedMsgs: CachedMsgs
660
661  proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
662    var retval: ModuleGraph
663    proc mockCommand(graph: ModuleGraph) =
664      retval = graph
665      let conf = graph.config
666      clearPasses(graph)
667      registerPass graph, verbosePass
668      registerPass graph, semPass
669      conf.setCmd cmdIdeTools
670      wantMainModule(conf)
671
672      if not fileExists(conf.projectFull):
673        quit "cannot find file: " & conf.projectFull.string
674
675      add(conf.searchPaths, conf.libpath)
676
677      conf.setErrorMaxHighMaybe
678      # do not print errors, but log them
679      conf.writelnHook = myLog
680      conf.structuredErrorHook = nil
681
682      # compile the project before showing any input so that we already
683      # can answer questions right away:
684      compileProject(graph)
685
686
687    proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
688      conf.suggestVersion = 0
689      let a = unixToNativePath(project)
690      if dirExists(a) and not fileExists(a.addFileExt("nim")):
691        conf.projectName = findProjectNimFile(conf, a)
692        # don't make it worse, report the error the old way:
693        if conf.projectName.len == 0: conf.projectName = a
694      else:
695        conf.projectName = a
696          # if processArgument(pass, p, argsCount): break
697    let
698      cache = newIdentCache()
699      conf = newConfigRef()
700      self = NimProg(
701        suggestMode: true,
702        processCmdLine: mockCmdLine
703      )
704    self.initDefinesProg(conf, "nimsuggest")
705
706    self.processCmdLineAndProjectPath(conf)
707
708    if gMode != mstdin:
709      conf.writelnHook = proc (msg: string) = discard
710    # Find Nim's prefix dir.
711    if nimPath == "":
712      let binaryPath = findExe("nim")
713      if binaryPath == "":
714        raise newException(IOError,
715            "Cannot find Nim standard library: Nim compiler not in PATH")
716      conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
717      if not dirExists(conf.prefixDir / RelativeDir"lib"):
718        conf.prefixDir = AbsoluteDir""
719    else:
720      conf.prefixDir = AbsoluteDir nimPath
721
722    #msgs.writelnHook = proc (line: string) = log(line)
723    myLog("START " & conf.projectFull.string)
724
725    var graph = newModuleGraph(cache, conf)
726    if self.loadConfigsAndProcessCmdLine(cache, conf, graph):
727      mockCommand(graph)
728    if gLogging:
729      for it in conf.searchPaths:
730        log(it.string)
731
732    retval.doStopCompile = proc (): bool = false
733    return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
734
735  proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
736    var retval: seq[Suggest] = @[]
737    let conf = nimsuggest.graph.config
738    conf.ideCmd = cmd
739    conf.writelnHook = proc (line: string) =
740      retval.add(Suggest(section: ideMsg, doc: line))
741    conf.suggestionResultHook = proc (s: Suggest) =
742      retval.add(s)
743    conf.writelnHook = proc (s: string) =
744      stderr.write s & "\n"
745    if conf.ideCmd == ideKnown:
746      retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
747    elif conf.ideCmd == ideProject:
748      retval.add(Suggest(section: ideProject, filePath: string conf.projectFull))
749    else:
750      if conf.ideCmd == ideChk:
751        for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
752      if conf.ideCmd == ideChk:
753        conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
754          retval.add(Suggest(section: ideChk, filePath: toFullPath(conf, info),
755            line: toLinenumber(info), column: toColumn(info), doc: msg,
756            forth: $sev))
757
758      else:
759        conf.structuredErrorHook = nil
760      executeNoHooks(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph)
761    return retval
762