nimsuggest.nim 18 KB

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