nimsuggest.nim 22 KB

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