nimsuggest.nim 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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 += skipWhitespace(cmd, i)
  390. if i < cmd.len and cmd[i] in {'0'..'9'}:
  391. orig = string conf.projectFull
  392. else:
  393. i = parseQuoted(cmd, orig, i)
  394. if i < cmd.len and cmd[i] == ';':
  395. i = parseQuoted(cmd, dirtyfile, i+1)
  396. i += skipWhile(cmd, seps, i)
  397. var line = -1
  398. var col = 0
  399. i += parseInt(cmd, line, i)
  400. i += skipWhile(cmd, seps, i)
  401. i += parseInt(cmd, col, i)
  402. if conf.ideCmd == ideKnown:
  403. results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig))))
  404. else:
  405. if conf.ideCmd == ideChk:
  406. for cm in cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
  407. execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, graph)
  408. sentinel()
  409. proc recompileFullProject(graph: ModuleGraph) =
  410. #echo "recompiling full project"
  411. resetSystemArtifacts(graph)
  412. graph.vm = nil
  413. graph.resetAllModules()
  414. GC_fullcollect()
  415. compileProject(graph)
  416. #echo GC_getStatistics()
  417. proc mainThread(graph: ModuleGraph) =
  418. let conf = graph.config
  419. if gLogging:
  420. for it in conf.searchPaths:
  421. log(it.string)
  422. proc wrHook(line: string) {.closure.} =
  423. if gMode == mepc:
  424. if gLogging: log(line)
  425. else:
  426. writelnToChannel(line)
  427. conf.writelnHook = wrHook
  428. conf.suggestionResultHook = sugResultHook
  429. graph.doStopCompile = proc (): bool = requests.peek() > 0
  430. var idle = 0
  431. var cachedMsgs: CachedMsgs = @[]
  432. while true:
  433. let (hasData, req) = requests.tryRecv()
  434. if hasData:
  435. conf.writelnHook = wrHook
  436. conf.suggestionResultHook = sugResultHook
  437. execCmd(req, graph, cachedMsgs)
  438. idle = 0
  439. else:
  440. os.sleep 250
  441. idle += 1
  442. if idle == 20 and gRefresh:
  443. # we use some nimsuggest activity to enable a lazy recompile:
  444. conf.ideCmd = ideChk
  445. conf.writelnHook = proc (s: string) = discard
  446. cachedMsgs.setLen 0
  447. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  448. cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev))
  449. conf.suggestionResultHook = proc (s: Suggest) = discard
  450. recompileFullProject(graph)
  451. var
  452. inputThread: Thread[ThreadParams]
  453. proc mainCommand(graph: ModuleGraph) =
  454. let conf = graph.config
  455. clearPasses(graph)
  456. registerPass graph, verbosePass
  457. registerPass graph, semPass
  458. conf.cmd = cmdIdeTools
  459. wantMainModule(conf)
  460. if not fileExists(conf.projectFull):
  461. quit "cannot find file: " & conf.projectFull.string
  462. add(conf.searchPaths, conf.libpath)
  463. # do not stop after the first error:
  464. conf.errorMax = high(int)
  465. # do not print errors, but log them
  466. conf.writelnHook = myLog
  467. conf.structuredErrorHook = nil
  468. # compile the project before showing any input so that we already
  469. # can answer questions right away:
  470. compileProject(graph)
  471. open(requests)
  472. open(results)
  473. case gMode
  474. of mstdin: createThread(inputThread, replStdin, (gPort, gAddress))
  475. of mtcp: createThread(inputThread, replTcp, (gPort, gAddress))
  476. of mepc: createThread(inputThread, replEpc, (gPort, gAddress))
  477. of mcmdsug: createThread(inputThread, replCmdline,
  478. (gPort, "sug \"" & conf.projectFull.string & "\":" & gAddress))
  479. of mcmdcon: createThread(inputThread, replCmdline,
  480. (gPort, "con \"" & conf.projectFull.string & "\":" & gAddress))
  481. mainThread(graph)
  482. joinThread(inputThread)
  483. close(requests)
  484. close(results)
  485. proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
  486. var p = parseopt.initOptParser(cmd)
  487. while true:
  488. parseopt.next(p)
  489. case p.kind
  490. of cmdEnd: break
  491. of cmdLongoption, cmdShortOption:
  492. case p.key.normalize
  493. of "help", "h":
  494. stdout.writeline(Usage)
  495. quit()
  496. of "autobind":
  497. gMode = mtcp
  498. gAutoBind = true
  499. of "port":
  500. gPort = parseInt(p.val).Port
  501. gMode = mtcp
  502. of "address":
  503. gAddress = p.val
  504. gMode = mtcp
  505. of "stdin": gMode = mstdin
  506. of "cmdsug":
  507. gMode = mcmdsug
  508. gAddress = p.val
  509. incl(conf.globalOptions, optIdeDebug)
  510. of "cmdcon":
  511. gMode = mcmdcon
  512. gAddress = p.val
  513. incl(conf.globalOptions, optIdeDebug)
  514. of "epc":
  515. gMode = mepc
  516. conf.verbosity = 0 # Port number gotta be first.
  517. of "debug": incl(conf.globalOptions, optIdeDebug)
  518. of "v2": conf.suggestVersion = 0
  519. of "v1": conf.suggestVersion = 1
  520. of "tester":
  521. gMode = mstdin
  522. gEmitEof = true
  523. gRefresh = false
  524. of "log": gLogging = true
  525. of "refresh":
  526. if p.val.len > 0:
  527. gRefresh = parseBool(p.val)
  528. else:
  529. gRefresh = true
  530. of "maxresults":
  531. conf.suggestMaxResults = parseInt(p.val)
  532. else: processSwitch(pass, p, conf)
  533. of cmdArgument:
  534. let a = unixToNativePath(p.key)
  535. if dirExists(a) and not fileExists(a.addFileExt("nim")):
  536. conf.projectName = findProjectNimFile(conf, a)
  537. # don't make it worse, report the error the old way:
  538. if conf.projectName.len == 0: conf.projectName = a
  539. else:
  540. conf.projectName = a
  541. # if processArgument(pass, p, argsCount): break
  542. proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
  543. let self = NimProg(
  544. suggestMode: true,
  545. processCmdLine: processCmdLine,
  546. mainCommand: mainCommand
  547. )
  548. self.initDefinesProg(conf, "nimsuggest")
  549. if paramCount() == 0:
  550. stdout.writeline(Usage)
  551. return
  552. self.processCmdLineAndProjectPath(conf)
  553. if gMode != mstdin:
  554. conf.writelnHook = proc (msg: string) = discard
  555. # Find Nim's prefix dir.
  556. let binaryPath = findExe("nim")
  557. if binaryPath == "":
  558. raise newException(IOError,
  559. "Cannot find Nim standard library: Nim compiler not in PATH")
  560. conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
  561. if not dirExists(conf.prefixDir / RelativeDir"lib"):
  562. conf.prefixDir = AbsoluteDir""
  563. #msgs.writelnHook = proc (line: string) = log(line)
  564. myLog("START " & conf.projectFull.string)
  565. discard self.loadConfigsAndRunMainCommand(cache, conf)
  566. when isMainModule:
  567. handleCmdline(newIdentCache(), newConfigRef())
  568. else:
  569. export Suggest
  570. export IdeCmd
  571. export AbsoluteFile
  572. type NimSuggest* = ref object
  573. graph: ModuleGraph
  574. idle: int
  575. cachedMsgs: CachedMsgs
  576. proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
  577. var retval: ModuleGraph
  578. proc mockCommand(graph: ModuleGraph) =
  579. retval = graph
  580. let conf = graph.config
  581. clearPasses(graph)
  582. registerPass graph, verbosePass
  583. registerPass graph, semPass
  584. conf.cmd = cmdIdeTools
  585. wantMainModule(conf)
  586. if not fileExists(conf.projectFull):
  587. quit "cannot find file: " & conf.projectFull.string
  588. add(conf.searchPaths, conf.libpath)
  589. # do not stop after the first error:
  590. conf.errorMax = high(int)
  591. # do not print errors, but log them
  592. conf.writelnHook = myLog
  593. conf.structuredErrorHook = nil
  594. # compile the project before showing any input so that we already
  595. # can answer questions right away:
  596. compileProject(graph)
  597. proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
  598. conf.suggestVersion = 0
  599. let a = unixToNativePath(project)
  600. if dirExists(a) and not fileExists(a.addFileExt("nim")):
  601. conf.projectName = findProjectNimFile(conf, a)
  602. # don't make it worse, report the error the old way:
  603. if conf.projectName.len == 0: conf.projectName = a
  604. else:
  605. conf.projectName = a
  606. # if processArgument(pass, p, argsCount): break
  607. let
  608. cache = newIdentCache()
  609. conf = newConfigRef()
  610. self = NimProg(
  611. suggestMode: true,
  612. processCmdLine: mockCmdLine,
  613. mainCommand: mockCommand
  614. )
  615. self.initDefinesProg(conf, "nimsuggest")
  616. self.processCmdLineAndProjectPath(conf)
  617. if gMode != mstdin:
  618. conf.writelnHook = proc (msg: string) = discard
  619. # Find Nim's prefix dir.
  620. if nimPath == "":
  621. let binaryPath = findExe("nim")
  622. if binaryPath == "":
  623. raise newException(IOError,
  624. "Cannot find Nim standard library: Nim compiler not in PATH")
  625. conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
  626. if not dirExists(conf.prefixDir / RelativeDir"lib"):
  627. conf.prefixDir = AbsoluteDir""
  628. else:
  629. conf.prefixDir = AbsoluteDir nimPath
  630. #msgs.writelnHook = proc (line: string) = log(line)
  631. myLog("START " & conf.projectFull.string)
  632. discard self.loadConfigsAndRunMainCommand(cache, conf)
  633. if gLogging:
  634. for it in conf.searchPaths:
  635. log(it.string)
  636. retval.doStopCompile = proc (): bool = false
  637. return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
  638. proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
  639. var retval: seq[Suggest] = @[]
  640. let conf = nimsuggest.graph.config
  641. conf.ideCmd = cmd
  642. conf.writelnHook = proc (line: string) =
  643. retval.add(Suggest(section: ideMsg, doc: line))
  644. conf.suggestionResultHook = proc (s: Suggest) =
  645. retval.add(s)
  646. conf.writelnHook = proc (s: string) =
  647. stderr.write s & "\n"
  648. if conf.ideCmd == ideKnown:
  649. retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
  650. else:
  651. if conf.ideCmd == ideChk:
  652. for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
  653. if conf.ideCmd == ideChk:
  654. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  655. retval.add(Suggest(section: ideChk, filePath: toFullPath(conf, info),
  656. line: toLinenumber(info), column: toColumn(info), doc: msg,
  657. forth: $sev))
  658. else:
  659. conf.structuredErrorHook = nil
  660. executeNoHooks(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph)
  661. return retval