nimsuggest.nim 23 KB

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