nimsuggest.nim 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256
  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. import compiler/renderer
  10. import strformat
  11. import algorithm
  12. import tables
  13. import times
  14. import procmonitor
  15. template tryImport(module) = import module
  16. when compiles tryImport ../dist/checksums/src/checksums/sha1:
  17. import ../dist/checksums/src/checksums/sha1
  18. else:
  19. import checksums/sha1
  20. ## Nimsuggest is a tool that helps to give editors IDE like capabilities.
  21. when not defined(nimcore):
  22. {.error: "nimcore MUST be defined for Nim's core tooling".}
  23. import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp
  24. # Do NOT import suggest. It will lead to weird bugs with
  25. # suggestionResultHook, because suggest.nim is included by sigmatch.
  26. # So we import that one instead.
  27. import compiler / [options, commands, modules,
  28. passes, passaux, msgs,
  29. sigmatch, ast,
  30. idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper,
  31. pathutils, condsyms, syntaxes]
  32. when defined(nimPreviewSlimSystem):
  33. import std/typedthreads
  34. when defined(windows):
  35. import winlean
  36. else:
  37. import posix
  38. const HighestSuggestProtocolVersion = 4
  39. const DummyEof = "!EOF!"
  40. const Usage = """
  41. Nimsuggest - Tool to give every editor IDE like capabilities for Nim
  42. Usage:
  43. nimsuggest [options] projectfile.nim
  44. Options:
  45. --autobind automatically binds into a free port
  46. --port:PORT port, by default 6000
  47. --address:HOST binds to that address, by default ""
  48. --stdin read commands from stdin and write results to
  49. stdout instead of using sockets
  50. --clientProcessId:PID shutdown nimsuggest in case this process dies
  51. --epc use emacs epc mode
  52. --debug enable debug output
  53. --log enable verbose logging to nimsuggest.log file
  54. --v1 use version 1 of the protocol; for backwards compatibility
  55. --v2 use version 2(default) of the protocol
  56. --v3 use version 3 of the protocol
  57. --v4 use version 4 of the protocol
  58. --info:X information
  59. --info:nimVer return the Nim compiler version that nimsuggest uses internally
  60. --info:protocolVer return the newest protocol version that is supported
  61. --info:capabilities return the capabilities supported by nimsuggest
  62. --refresh perform automatic refreshes to keep the analysis precise
  63. --maxresults:N limit the number of suggestions to N
  64. --tester implies --stdin and outputs a line
  65. '""" & DummyEof & """' for the tester
  66. --find attempts to find the project file of the current project
  67. The server then listens to the connection and takes line-based commands.
  68. If --autobind is used, the binded port number will be printed to stdout.
  69. In addition, all command line options of Nim that do not affect code generation
  70. are supported.
  71. """
  72. type
  73. Mode = enum mstdin, mtcp, mepc, mcmdsug, mcmdcon
  74. CachedMsg = object
  75. info: TLineInfo
  76. msg: string
  77. sev: Severity
  78. CachedMsgs = seq[CachedMsg]
  79. var
  80. gPort = 6000.Port
  81. gAddress = "127.0.0.1"
  82. gMode: Mode
  83. gEmitEof: bool # whether we write '!EOF!' dummy lines
  84. gLogging = defined(logging)
  85. gRefresh: bool
  86. gAutoBind = false
  87. requests: Channel[string]
  88. results: Channel[Suggest]
  89. proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string,
  90. graph: ModuleGraph);
  91. proc writelnToChannel(line: string) =
  92. results.send(Suggest(section: ideMsg, doc: line))
  93. proc sugResultHook(s: Suggest) =
  94. results.send(s)
  95. proc errorHook(conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  96. results.send(Suggest(section: ideChk, filePath: toFullPath(conf, info),
  97. line: toLinenumber(info), column: toColumn(info), doc: msg,
  98. forth: $sev))
  99. proc myLog(s: string) =
  100. if gLogging: log(s)
  101. const
  102. seps = {':', ';', ' ', '\t'}
  103. Help = "usage: sug|con|def|use|dus|chk|mod|highlight|outline|known|project file.nim[;dirtyfile.nim]:line:col\n" &
  104. "type 'quit' to quit\n" &
  105. "type 'debug' to toggle debug mode on/off\n" &
  106. "type 'terse' to toggle terse mode on/off"
  107. #List of currently supported capabilities. So lang servers/ides can iterate over and check for what's enabled
  108. Capabilities = [
  109. "con", #current NimSuggest supports the `con` commmand
  110. "unknownFile", #current NimSuggest can handle unknown files
  111. ]
  112. proc parseQuoted(cmd: string; outp: var string; start: int): int =
  113. var i = start
  114. i += skipWhitespace(cmd, i)
  115. if i < cmd.len and cmd[i] == '"':
  116. i += parseUntil(cmd, outp, '"', i+1)+2
  117. else:
  118. i += parseUntil(cmd, outp, seps, i)
  119. result = i
  120. proc sexp(s: IdeCmd|TSymKind|PrefixMatch): SexpNode = sexp($s)
  121. proc sexp(s: Suggest): SexpNode =
  122. # If you change the order here, make sure to change it over in
  123. # nim-mode.el too.
  124. let qp = if s.qualifiedPath.len == 0: @[] else: s.qualifiedPath
  125. result = convertSexp([
  126. s.section,
  127. TSymKind s.symkind,
  128. qp.map(newSString),
  129. s.filePath,
  130. s.forth,
  131. s.line,
  132. s.column,
  133. s.doc,
  134. s.quality
  135. ])
  136. if s.section == ideSug:
  137. result.add convertSexp(s.prefix)
  138. if s.section in {ideOutline, ideExpand} and s.version == 3:
  139. result.add convertSexp(s.endLine.int)
  140. result.add convertSexp(s.endCol)
  141. proc sexp(s: seq[Suggest]): SexpNode =
  142. result = newSList()
  143. for sug in s:
  144. result.add(sexp(sug))
  145. proc listEpc(): SexpNode =
  146. # This function is called from Emacs to show available options.
  147. let
  148. argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol))
  149. docstring = sexp("line starts at 1, column at 0, dirtyfile is optional")
  150. result = newSList()
  151. for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration", "inlayHints"]:
  152. let
  153. cmd = sexp(command)
  154. methodDesc = newSList()
  155. methodDesc.add(cmd)
  156. methodDesc.add(argspecs)
  157. methodDesc.add(docstring)
  158. result.add(methodDesc)
  159. proc findNode(n: PNode; trackPos: TLineInfo): PSym =
  160. #echo "checking node ", n.info
  161. if n.kind == nkSym:
  162. if isTracked(n.info, trackPos, n.sym.name.s.len): return n.sym
  163. else:
  164. for i in 0 ..< safeLen(n):
  165. let res = findNode(n[i], trackPos)
  166. if res != nil: return res
  167. proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo): PSym =
  168. let m = graph.getModule(trackPos.fileIndex)
  169. if m != nil and m.ast != nil:
  170. result = findNode(m.ast, trackPos)
  171. template benchmark(benchmarkName: untyped, code: untyped) =
  172. block:
  173. myLog "Started [" & benchmarkName & "]..."
  174. let t0 = epochTime()
  175. code
  176. let elapsed = epochTime() - t0
  177. let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 3)
  178. myLog "CPU Time [" & benchmarkName & "] " & elapsedStr & "s"
  179. proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, tag: string,
  180. graph: ModuleGraph) =
  181. let conf = graph.config
  182. if conf.suggestVersion >= 3:
  183. let command = fmt "cmd = {cmd} {file}:{line}:{col}"
  184. benchmark command:
  185. executeNoHooksV3(cmd, file, dirtyfile, line, col, tag, graph)
  186. return
  187. myLog("cmd: " & $cmd & ", file: " & file.string &
  188. ", dirtyFile: " & dirtyfile.string &
  189. "[" & $line & ":" & $col & "]")
  190. conf.ideCmd = cmd
  191. if cmd == ideUse and conf.suggestVersion != 0:
  192. graph.resetAllModules()
  193. var isKnownFile = true
  194. let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
  195. if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile)
  196. else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"")
  197. conf.m.trackPos = newLineInfo(dirtyIdx, line, col)
  198. conf.m.trackPosAttached = false
  199. conf.errorCounter = 0
  200. if conf.suggestVersion == 1:
  201. graph.usageSym = nil
  202. if not isKnownFile:
  203. graph.compileProject(dirtyIdx)
  204. if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and
  205. dirtyfile.isEmpty:
  206. discard "no need to recompile anything"
  207. else:
  208. let modIdx = graph.parentModule(dirtyIdx)
  209. graph.markDirty dirtyIdx
  210. graph.markClientsDirty dirtyIdx
  211. if conf.ideCmd != ideMod:
  212. if isKnownFile:
  213. graph.compileProject(modIdx)
  214. if conf.ideCmd in {ideUse, ideDus}:
  215. let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym
  216. if u != nil:
  217. listUsages(graph, u)
  218. else:
  219. localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos))
  220. proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, graph: ModuleGraph) =
  221. executeNoHooks(cmd, file, dirtyfile, line, col, "", graph)
  222. proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; tag: string,
  223. graph: ModuleGraph) =
  224. if cmd == ideChk:
  225. graph.config.structuredErrorHook = errorHook
  226. graph.config.writelnHook = myLog
  227. else:
  228. graph.config.structuredErrorHook = nil
  229. graph.config.writelnHook = myLog
  230. executeNoHooks(cmd, file, dirtyfile, line, col, tag, graph)
  231. proc executeEpc(cmd: IdeCmd, args: SexpNode;
  232. graph: ModuleGraph) =
  233. let
  234. file = AbsoluteFile args[0].getStr
  235. line = args[1].getNum
  236. column = args[2].getNum
  237. var dirtyfile = AbsoluteFile""
  238. if len(args) > 3:
  239. dirtyfile = AbsoluteFile args[3].getStr("")
  240. execute(cmd, file, dirtyfile, int(line), int(column), args[3].getStr, graph)
  241. proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string,
  242. returnSymbol = "return") =
  243. let response = $convertSexp([newSSymbol(returnSymbol), uid, s])
  244. socket.send(toHex(len(response), 6))
  245. socket.send(response)
  246. template checkSanity(client, sizeHex, size, messageBuffer: typed) =
  247. if client.recv(sizeHex, 6) != 6:
  248. raise newException(ValueError, "didn't get all the hexbytes")
  249. if parseHex(sizeHex, size) == 0:
  250. raise newException(ValueError, "invalid size hex: " & $sizeHex)
  251. if client.recv(messageBuffer, size) != size:
  252. raise newException(ValueError, "didn't get all the bytes")
  253. proc toStdout() {.gcsafe.} =
  254. while true:
  255. let res = results.recv()
  256. case res.section
  257. of ideNone: break
  258. of ideMsg: echo res.doc
  259. of ideKnown: echo res.quality == 1
  260. of ideProject: echo res.filePath
  261. else: echo res
  262. proc toSocket(stdoutSocket: Socket) {.gcsafe.} =
  263. while true:
  264. let res = results.recv()
  265. case res.section
  266. of ideNone: break
  267. of ideMsg: stdoutSocket.send(res.doc & "\c\L")
  268. of ideKnown: stdoutSocket.send($(res.quality == 1) & "\c\L")
  269. of ideProject: stdoutSocket.send(res.filePath & "\c\L")
  270. else: stdoutSocket.send($res & "\c\L")
  271. proc toEpc(client: Socket; uid: BiggestInt) {.gcsafe.} =
  272. var list = newSList()
  273. while true:
  274. let res = results.recv()
  275. case res.section
  276. of ideNone: break
  277. of ideMsg:
  278. list.add sexp(res.doc)
  279. of ideKnown:
  280. list.add sexp(res.quality == 1)
  281. of ideProject:
  282. list.add sexp(res.filePath)
  283. else:
  284. list.add sexp(res)
  285. returnEpc(client, uid, list)
  286. template setVerbosity(level: typed) =
  287. gVerbosity = level
  288. conf.notes = NotesVerbosity[gVerbosity]
  289. proc connectToNextFreePort(server: Socket, host: string): Port =
  290. server.bindAddr(Port(0), host)
  291. let (_, port) = server.getLocalAddr
  292. result = port
  293. type
  294. ThreadParams = tuple[port: Port; address: string]
  295. proc replStdinSingleCmd(line: string) =
  296. requests.send line
  297. toStdout()
  298. echo ""
  299. flushFile(stdout)
  300. proc replStdin(x: ThreadParams) {.thread.} =
  301. if gEmitEof:
  302. echo DummyEof
  303. while true:
  304. let line = readLine(stdin)
  305. requests.send line
  306. if line == "quit": break
  307. toStdout()
  308. echo DummyEof
  309. flushFile(stdout)
  310. else:
  311. echo Help
  312. var line = ""
  313. while readLineFromStdin("> ", line):
  314. replStdinSingleCmd(line)
  315. requests.send "quit"
  316. proc replCmdline(x: ThreadParams) {.thread.} =
  317. replStdinSingleCmd(x.address)
  318. requests.send "quit"
  319. proc replTcp(x: ThreadParams) {.thread.} =
  320. var server = newSocket()
  321. if gAutoBind:
  322. let port = server.connectToNextFreePort(x.address)
  323. server.listen()
  324. echo port
  325. stdout.flushFile()
  326. else:
  327. server.bindAddr(x.port, x.address)
  328. server.listen()
  329. var inp = ""
  330. var stdoutSocket: Socket
  331. while true:
  332. accept(server, stdoutSocket)
  333. stdoutSocket.readLine(inp)
  334. requests.send inp
  335. toSocket(stdoutSocket)
  336. stdoutSocket.send("\c\L")
  337. stdoutSocket.close()
  338. proc argsToStr(x: SexpNode): string =
  339. if x.kind != SList: return x.getStr
  340. doAssert x.kind == SList
  341. doAssert x.len >= 4
  342. let file = x[0].getStr
  343. let line = x[1].getNum
  344. let col = x[2].getNum
  345. let dirty = x[3].getStr
  346. result = x[0].getStr.escape
  347. if dirty.len > 0:
  348. result.add ';'
  349. result.add dirty.escape
  350. result.add ':'
  351. result.addInt line
  352. result.add ':'
  353. result.addInt col
  354. proc replEpc(x: ThreadParams) {.thread.} =
  355. var server = newSocket()
  356. let port = connectToNextFreePort(server, "localhost")
  357. server.listen()
  358. echo port
  359. stdout.flushFile()
  360. var client: Socket
  361. # Wait for connection
  362. accept(server, client)
  363. while true:
  364. var
  365. sizeHex = ""
  366. size = 0
  367. messageBuffer = ""
  368. checkSanity(client, sizeHex, size, messageBuffer)
  369. let
  370. message = parseSexp($messageBuffer)
  371. epcApi = message[0].getSymbol
  372. case epcApi
  373. of "call":
  374. let
  375. uid = message[1].getNum
  376. cmd = message[2].getSymbol
  377. args = message[3]
  378. when false:
  379. x.ideCmd[] = parseIdeCmd(message[2].getSymbol)
  380. case x.ideCmd[]
  381. of ideSug, ideCon, ideDef, ideUse, ideDus, ideOutline, ideHighlight:
  382. setVerbosity(0)
  383. else: discard
  384. let fullCmd = cmd & " " & args.argsToStr
  385. myLog "MSG CMD: " & fullCmd
  386. requests.send(fullCmd)
  387. toEpc(client, uid)
  388. of "methods":
  389. returnEpc(client, message[1].getNum, listEpc())
  390. of "epc-error":
  391. # an unhandled exception forces down the whole process anyway, so we
  392. # use 'quit' here instead of 'raise'
  393. quit("received epc error: " & $messageBuffer)
  394. else:
  395. let errMessage = case epcApi
  396. of "return", "return-error":
  397. "no return expected"
  398. else:
  399. "unexpected call: " & epcApi
  400. quit errMessage
  401. proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) =
  402. let conf = graph.config
  403. template sentinel() =
  404. # send sentinel for the input reading thread:
  405. results.send(Suggest(section: ideNone))
  406. template toggle(sw) =
  407. if sw in conf.globalOptions:
  408. excl(conf.globalOptions, sw)
  409. else:
  410. incl(conf.globalOptions, sw)
  411. sentinel()
  412. return
  413. template err() =
  414. echo Help
  415. sentinel()
  416. return
  417. var opc = ""
  418. var i = parseIdent(cmd, opc, 0)
  419. case opc.normalize
  420. of "sug": conf.ideCmd = ideSug
  421. of "con": conf.ideCmd = ideCon
  422. of "def": conf.ideCmd = ideDef
  423. of "use": conf.ideCmd = ideUse
  424. of "dus": conf.ideCmd = ideDus
  425. of "mod": conf.ideCmd = ideMod
  426. of "chk": conf.ideCmd = ideChk
  427. of "highlight": conf.ideCmd = ideHighlight
  428. of "outline": conf.ideCmd = ideOutline
  429. of "quit":
  430. sentinel()
  431. quit()
  432. of "debug": toggle optIdeDebug
  433. of "terse": toggle optIdeTerse
  434. of "known": conf.ideCmd = ideKnown
  435. of "project": conf.ideCmd = ideProject
  436. of "changed": conf.ideCmd = ideChanged
  437. of "globalsymbols": conf.ideCmd = ideGlobalSymbols
  438. of "declaration": conf.ideCmd = ideDeclaration
  439. of "expand": conf.ideCmd = ideExpand
  440. of "chkfile": conf.ideCmd = ideChkFile
  441. of "recompile": conf.ideCmd = ideRecompile
  442. of "type": conf.ideCmd = ideType
  443. of "inlayhints":
  444. if conf.suggestVersion >= 4:
  445. conf.ideCmd = ideInlayHints
  446. else:
  447. err()
  448. else: err()
  449. var dirtyfile = ""
  450. var orig = ""
  451. i += skipWhitespace(cmd, i)
  452. if i < cmd.len and cmd[i] in {'0'..'9'}:
  453. orig = string conf.projectFull
  454. else:
  455. i = parseQuoted(cmd, orig, i)
  456. if i < cmd.len and cmd[i] == ';':
  457. i = parseQuoted(cmd, dirtyfile, i+1)
  458. i += skipWhile(cmd, seps, i)
  459. var line = 0
  460. var col = -1
  461. i += parseInt(cmd, line, i)
  462. i += skipWhile(cmd, seps, i)
  463. i += parseInt(cmd, col, i)
  464. let tag = substr(cmd, i)
  465. if conf.ideCmd == ideKnown:
  466. results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig))))
  467. elif conf.ideCmd == ideProject:
  468. results.send(Suggest(section: ideProject, filePath: string conf.projectFull))
  469. else:
  470. if conf.ideCmd == ideChk:
  471. for cm in cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
  472. execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, tag, graph)
  473. sentinel()
  474. proc recompileFullProject(graph: ModuleGraph) =
  475. benchmark "Recompilation(clean)":
  476. graph.resetForBackend()
  477. graph.resetSystemArtifacts()
  478. graph.vm = nil
  479. graph.resetAllModules()
  480. GC_fullCollect()
  481. graph.compileProject()
  482. proc mainThread(graph: ModuleGraph) =
  483. let conf = graph.config
  484. myLog "searchPaths: "
  485. for it in conf.searchPaths:
  486. myLog(" " & it.string)
  487. proc wrHook(line: string) {.closure.} =
  488. if gMode == mepc:
  489. if gLogging: log(line)
  490. else:
  491. writelnToChannel(line)
  492. conf.writelnHook = wrHook
  493. conf.suggestionResultHook = sugResultHook
  494. graph.doStopCompile = proc (): bool = requests.peek() > 0
  495. var idle = 0
  496. var cachedMsgs: CachedMsgs = @[]
  497. while true:
  498. let (hasData, req) = requests.tryRecv()
  499. if hasData:
  500. conf.writelnHook = wrHook
  501. conf.suggestionResultHook = sugResultHook
  502. execCmd(req, graph, cachedMsgs)
  503. idle = 0
  504. else:
  505. os.sleep 250
  506. idle += 1
  507. if idle == 20 and gRefresh and conf.suggestVersion < 3:
  508. # we use some nimsuggest activity to enable a lazy recompile:
  509. conf.ideCmd = ideChk
  510. conf.writelnHook = proc (s: string) = discard
  511. cachedMsgs.setLen 0
  512. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  513. cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev))
  514. conf.suggestionResultHook = proc (s: Suggest) = discard
  515. recompileFullProject(graph)
  516. var
  517. inputThread: Thread[ThreadParams]
  518. proc mainCommand(graph: ModuleGraph) =
  519. let conf = graph.config
  520. clearPasses(graph)
  521. registerPass graph, verbosePass
  522. registerPass graph, semPass
  523. conf.setCmd cmdIdeTools
  524. defineSymbol(conf.symbols, $conf.backend)
  525. wantMainModule(conf)
  526. if not fileExists(conf.projectFull):
  527. quit "cannot find file: " & conf.projectFull.string
  528. add(conf.searchPaths, conf.libpath)
  529. conf.setErrorMaxHighMaybe # honor --errorMax even if it may not make sense here
  530. # do not print errors, but log them
  531. conf.writelnHook = proc (msg: string) = discard
  532. if graph.config.suggestVersion >= 3:
  533. graph.config.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  534. let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info),
  535. line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev)
  536. graph.suggestErrors.mgetOrPut(info.fileIndex, @[]).add suggest
  537. # compile the project before showing any input so that we already
  538. # can answer questions right away:
  539. benchmark "Initial compilation":
  540. compileProject(graph)
  541. open(requests)
  542. open(results)
  543. if graph.config.clientProcessId != 0:
  544. hookProcMonitor(graph.config.clientProcessId)
  545. case gMode
  546. of mstdin: createThread(inputThread, replStdin, (gPort, gAddress))
  547. of mtcp: createThread(inputThread, replTcp, (gPort, gAddress))
  548. of mepc: createThread(inputThread, replEpc, (gPort, gAddress))
  549. of mcmdsug: createThread(inputThread, replCmdline,
  550. (gPort, "sug \"" & conf.projectFull.string & "\":" & gAddress))
  551. of mcmdcon: createThread(inputThread, replCmdline,
  552. (gPort, "con \"" & conf.projectFull.string & "\":" & gAddress))
  553. mainThread(graph)
  554. joinThread(inputThread)
  555. close(requests)
  556. close(results)
  557. proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
  558. var p = parseopt.initOptParser(cmd)
  559. var findProject = false
  560. while true:
  561. parseopt.next(p)
  562. case p.kind
  563. of cmdEnd: break
  564. of cmdLongOption, cmdShortOption:
  565. case p.key.normalize
  566. of "help", "h":
  567. stdout.writeLine(Usage)
  568. quit()
  569. of "autobind":
  570. gMode = mtcp
  571. gAutoBind = true
  572. of "port":
  573. gPort = parseInt(p.val).Port
  574. gMode = mtcp
  575. of "address":
  576. gAddress = p.val
  577. gMode = mtcp
  578. of "stdin": gMode = mstdin
  579. of "cmdsug":
  580. gMode = mcmdsug
  581. gAddress = p.val
  582. incl(conf.globalOptions, optIdeDebug)
  583. of "cmdcon":
  584. gMode = mcmdcon
  585. gAddress = p.val
  586. incl(conf.globalOptions, optIdeDebug)
  587. of "epc":
  588. gMode = mepc
  589. conf.verbosity = 0 # Port number gotta be first.
  590. of "debug": incl(conf.globalOptions, optIdeDebug)
  591. of "v1": conf.suggestVersion = 1
  592. of "v2": conf.suggestVersion = 0
  593. of "v3": conf.suggestVersion = 3
  594. of "v4": conf.suggestVersion = 4
  595. of "info":
  596. case p.val.normalize
  597. of "protocolver":
  598. stdout.writeLine(HighestSuggestProtocolVersion)
  599. quit 0
  600. of "nimver":
  601. stdout.writeLine(system.NimVersion)
  602. quit 0
  603. of "capabilities":
  604. stdout.writeLine(Capabilities.toSeq.mapIt($it).join(" "))
  605. quit 0
  606. else:
  607. processSwitch(pass, p, conf)
  608. of "tester":
  609. gMode = mstdin
  610. gEmitEof = true
  611. gRefresh = false
  612. of "log": gLogging = true
  613. of "refresh":
  614. if p.val.len > 0:
  615. gRefresh = parseBool(p.val)
  616. else:
  617. gRefresh = true
  618. of "maxresults":
  619. conf.suggestMaxResults = parseInt(p.val)
  620. of "find":
  621. findProject = true
  622. of "clientprocessid":
  623. conf.clientProcessId = parseInt(p.val)
  624. else: processSwitch(pass, p, conf)
  625. of cmdArgument:
  626. let a = unixToNativePath(p.key)
  627. if dirExists(a) and not fileExists(a.addFileExt("nim")):
  628. conf.projectName = findProjectNimFile(conf, a)
  629. # don't make it worse, report the error the old way:
  630. if conf.projectName.len == 0: conf.projectName = a
  631. else:
  632. if findProject:
  633. conf.projectName = findProjectNimFile(conf, a.parentDir())
  634. if conf.projectName.len == 0:
  635. conf.projectName = a
  636. else:
  637. conf.projectName = a
  638. # if processArgument(pass, p, argsCount): break
  639. proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
  640. let self = NimProg(
  641. suggestMode: true,
  642. processCmdLine: processCmdLine
  643. )
  644. self.initDefinesProg(conf, "nimsuggest")
  645. if paramCount() == 0:
  646. stdout.writeLine(Usage)
  647. return
  648. self.processCmdLineAndProjectPath(conf)
  649. if gMode != mstdin:
  650. conf.writelnHook = proc (msg: string) = discard
  651. # Find Nim's prefix dir.
  652. let binaryPath = findExe("nim")
  653. if binaryPath == "":
  654. raise newException(IOError,
  655. "Cannot find Nim standard library: Nim compiler not in PATH")
  656. conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
  657. if not dirExists(conf.prefixDir / RelativeDir"lib"):
  658. conf.prefixDir = AbsoluteDir""
  659. #msgs.writelnHook = proc (line: string) = log(line)
  660. myLog("START " & conf.projectFull.string)
  661. var graph = newModuleGraph(cache, conf)
  662. if self.loadConfigsAndProcessCmdLine(cache, conf, graph):
  663. mainCommand(graph)
  664. # v3 start
  665. proc recompilePartially(graph: ModuleGraph, projectFileIdx = InvalidFileIdx) =
  666. if projectFileIdx == InvalidFileIdx:
  667. myLog "Recompiling partially from root"
  668. else:
  669. myLog fmt "Recompiling partially starting from {graph.getModule(projectFileIdx)}"
  670. # inst caches are breaking incremental compilation when the cache caches stuff
  671. # from dirty buffer
  672. # TODO: investigate more efficient way to achieve the same
  673. # graph.typeInstCache.clear()
  674. # graph.procInstCache.clear()
  675. GC_fullCollect()
  676. try:
  677. benchmark "Recompilation":
  678. graph.compileProject(projectFileIdx)
  679. except Exception as e:
  680. myLog fmt "Failed to recompile partially with the following error:\n {e.msg} \n\n {e.getStackTrace()}"
  681. try:
  682. graph.recompileFullProject()
  683. except Exception as e:
  684. myLog fmt "Failed clean recompilation:\n {e.msg} \n\n {e.getStackTrace()}"
  685. func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair] =
  686. # xs contains duplicate items and we want to filter them by range because the
  687. # sym may not match. This can happen when xs contains the same definition but
  688. # with different signature because suggestSym might be called multiple times
  689. # for the same symbol (e. g. including/excluding the pragma)
  690. result = newSeqOfCap[SymInfoPair](xs.len)
  691. for itm in xs.reversed:
  692. var found = false
  693. for res in result:
  694. if res.info.exactEquals(itm.info):
  695. found = true
  696. break
  697. if not found:
  698. result.add(itm)
  699. result.reverse()
  700. proc findSymData(graph: ModuleGraph, trackPos: TLineInfo):
  701. ref SymInfoPair =
  702. for s in graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair:
  703. if isTracked(s.info, trackPos, s.sym.name.s.len):
  704. new(result)
  705. result[] = s
  706. break
  707. func isInRange*(current, startPos, endPos: TLineInfo, tokenLen: int): bool =
  708. result = current.fileIndex == startPos.fileIndex and
  709. (current.line > startPos.line or (current.line == startPos.line and current.col>=startPos.col)) and
  710. (current.line < endPos.line or (current.line == endPos.line and current.col <= endPos.col))
  711. proc findSymDataInRange(graph: ModuleGraph, startPos, endPos: TLineInfo):
  712. seq[SymInfoPair] =
  713. result = newSeq[SymInfoPair]()
  714. for s in graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair:
  715. if isInRange(s.info, startPos, endPos, s.sym.name.s.len):
  716. result.add(s)
  717. proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
  718. ref SymInfoPair =
  719. let
  720. fileIdx = fileInfoIdx(graph.config, file)
  721. trackPos = newLineInfo(fileIdx, line, col)
  722. result = findSymData(graph, trackPos)
  723. proc findSymDataInRange(graph: ModuleGraph, file: AbsoluteFile; startLine, startCol, endLine, endCol: int):
  724. seq[SymInfoPair] =
  725. let
  726. fileIdx = fileInfoIdx(graph.config, file)
  727. startPos = newLineInfo(fileIdx, startLine, startCol)
  728. endPos = newLineInfo(fileIdx, endLine, endCol)
  729. result = findSymDataInRange(graph, startPos, endPos)
  730. proc markDirtyIfNeeded(graph: ModuleGraph, file: string, originalFileIdx: FileIndex) =
  731. let sha = $sha1.secureHashFile(file)
  732. if graph.config.m.fileInfos[originalFileIdx.int32].hash != sha or graph.config.ideCmd == ideSug:
  733. myLog fmt "{file} changed compared to last compilation"
  734. graph.markDirty originalFileIdx
  735. graph.markClientsDirty originalFileIdx
  736. else:
  737. myLog fmt "No changes in file {file} compared to last compilation"
  738. proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
  739. defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
  740. let section = if defaultSection != ideNone:
  741. defaultSection
  742. elif sym.info.exactEquals(info):
  743. ideDef
  744. else:
  745. ideUse
  746. let suggest = symToSuggest(graph, sym, isLocal=false, section,
  747. info, 100, PrefixMatch.None, false, 0,
  748. endLine = endLine, endCol = endCol)
  749. suggestResult(graph.config, suggest)
  750. proc suggestInlayHintResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
  751. defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
  752. let section = if defaultSection != ideNone:
  753. defaultSection
  754. elif sym.info.exactEquals(info):
  755. ideDef
  756. else:
  757. ideUse
  758. var suggestDef = symToSuggest(graph, sym, isLocal=false, section,
  759. info, 100, PrefixMatch.None, false, 0, true,
  760. endLine = endLine, endCol = endCol)
  761. suggestDef.inlayHintInfo = suggestToSuggestInlayHint(suggestDef)
  762. suggestDef.section = ideInlayHints
  763. if sym.kind == skForVar:
  764. suggestDef.inlayHintInfo.allowInsert = false
  765. suggestResult(graph.config, suggestDef)
  766. const
  767. # kinds for ideOutline and ideGlobalSymbols
  768. searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate}
  769. proc symbolEqual(left, right: PSym): bool =
  770. # More relaxed symbol comparison
  771. return left.info.exactEquals(right.info) and left.name == right.name
  772. proc findDef(n: PNode, line: uint16, col: int16): PNode =
  773. if n.kind in {nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkMacroDef}:
  774. if n.info.line == line:
  775. return n
  776. else:
  777. for i in 0 ..< safeLen(n):
  778. let res = findDef(n[i], line, col)
  779. if res != nil: return res
  780. proc findByTLineInfo(trackPos: TLineInfo, infoPairs: seq[SymInfoPair]):
  781. ref SymInfoPair =
  782. for s in infoPairs:
  783. if s.info.exactEquals trackPos:
  784. new(result)
  785. result[] = s
  786. break
  787. proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: seq[SymInfoPair]): bool =
  788. proc checkSymbol(sym: PSym, info: TLineInfo): bool =
  789. result = (sym.owner.kind in {skModule, skType} or sym.kind in {skProc, skMethod, skIterator, skTemplate, skType})
  790. if n.kind == nkSym and n.sym.checkSymbol(n.info):
  791. graph.suggestResult(n.sym, n.sym.info, ideOutline, endInfo.line, endInfo.col)
  792. return true
  793. elif n.kind == nkIdent:
  794. let symData = findByTLineInfo(n.info, infoPairs)
  795. if symData != nil and symData.sym.checkSymbol(symData.info):
  796. let sym = symData.sym
  797. graph.suggestResult(sym, sym.info, ideOutline, endInfo.line, endInfo.col)
  798. return true
  799. proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: seq[SymInfoPair]): bool =
  800. for child in n:
  801. if child.kind in {nkIdent, nkSym}:
  802. if graph.outlineNode(child, endInfo, infoPairs):
  803. return true
  804. elif child.kind == nkPostfix:
  805. if graph.handleIdentOrSym(child, endInfo, infoPairs):
  806. return true
  807. proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: seq[SymInfoPair]) =
  808. var matched = true
  809. if n.kind == nkIdent:
  810. let symData = findByTLineInfo(n.info, infoPairs)
  811. if symData != nil and symData.sym.kind == skEnumField and symData.info.exactEquals(symData.sym.info):
  812. let sym = symData.sym
  813. graph.suggestResult(sym, sym.info, ideOutline, n.endInfo.line, n.endInfo.col)
  814. elif (n.kind in {nkFuncDef, nkProcDef, nkTypeDef, nkMacroDef, nkTemplateDef, nkConverterDef, nkEnumFieldDef, nkConstDef}):
  815. matched = handleIdentOrSym(graph, n, n.endInfo, infoPairs)
  816. else:
  817. matched = false
  818. if n.kind != nkFormalParams:
  819. for child in n:
  820. graph.iterateOutlineNodes(child, infoPairs)
  821. proc calculateExpandRange(n: PNode, info: TLineInfo): TLineInfo =
  822. if ((n.kind in {nkFuncDef, nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkConverterDef} and
  823. n.info.exactEquals(info)) or
  824. (n.kind in {nkCall, nkCommand} and n[0].info.exactEquals(info))):
  825. result = n.endInfo
  826. else:
  827. for child in n:
  828. result = child.calculateExpandRange(info)
  829. if result != unknownLineInfo:
  830. return result
  831. result = unknownLineInfo
  832. proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string,
  833. graph: ModuleGraph) =
  834. let conf = graph.config
  835. conf.writelnHook = proc (s: string) = discard
  836. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo;
  837. msg: string; sev: Severity) =
  838. let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info),
  839. line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev)
  840. graph.suggestErrors.mgetOrPut(info.fileIndex, @[]).add suggest
  841. conf.ideCmd = cmd
  842. myLog fmt "cmd: {cmd}, file: {file}[{line}:{col}], dirtyFile: {dirtyfile}, tag: {tag}"
  843. var fileIndex: FileIndex
  844. if not (cmd in {ideRecompile, ideGlobalSymbols}):
  845. fileIndex = fileInfoIdx(conf, file)
  846. msgs.setDirtyFile(
  847. conf,
  848. fileIndex,
  849. if dirtyfile.isEmpty: AbsoluteFile"" else: dirtyfile)
  850. if not dirtyfile.isEmpty:
  851. graph.markDirtyIfNeeded(dirtyFile.string, fileInfoIdx(conf, file))
  852. # these commands require fully compiled project
  853. if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk, ideInlayHints} and graph.needsCompilation():
  854. graph.recompilePartially()
  855. # when doing incremental build for the project root we should make sure that
  856. # everything is unmarked as no longer beeing dirty in case there is no
  857. # longer reference to a particular module. E. g. A depends on B, B is marked
  858. # as dirty and A loses B import.
  859. graph.unmarkAllDirty()
  860. # these commands require partially compiled project
  861. elif cmd in {ideSug, ideCon, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration, ideExpand} and
  862. (graph.needsCompilation(fileIndex) or cmd in {ideSug, ideCon}):
  863. # for ideSug use v2 implementation
  864. if cmd in {ideSug, ideCon}:
  865. conf.m.trackPos = newLineInfo(fileIndex, line, col)
  866. conf.m.trackPosAttached = false
  867. else:
  868. conf.m.trackPos = default(TLineInfo)
  869. graph.recompilePartially(fileIndex)
  870. case cmd
  871. of ideDef:
  872. let s = graph.findSymData(file, line, col)
  873. if not s.isNil:
  874. graph.suggestResult(s.sym, s.sym.info)
  875. of ideType:
  876. let s = graph.findSymData(file, line, col)
  877. if not s.isNil:
  878. let typeSym = s.sym.typ.sym
  879. if typeSym != nil:
  880. graph.suggestResult(typeSym, typeSym.info, ideType)
  881. elif s.sym.typ.len != 0:
  882. let genericType = s.sym.typ[0].sym
  883. graph.suggestResult(genericType, genericType.info, ideType)
  884. of ideUse, ideDus:
  885. let symbol = graph.findSymData(file, line, col)
  886. if not symbol.isNil:
  887. var res: seq[SymInfoPair] = @[]
  888. for s in graph.suggestSymbolsIter:
  889. if s.sym.symbolEqual(symbol.sym):
  890. res.add(s)
  891. for s in res.deduplicateSymInfoPair():
  892. graph.suggestResult(s.sym, s.info)
  893. of ideHighlight:
  894. let sym = graph.findSymData(file, line, col)
  895. if not sym.isNil:
  896. let usages = graph.fileSymbols(fileIndex).filterIt(it.sym == sym.sym)
  897. myLog fmt "Found {usages.len} usages in {file.string}"
  898. for s in usages:
  899. graph.suggestResult(s.sym, s.info)
  900. of ideRecompile:
  901. graph.recompileFullProject()
  902. of ideChanged:
  903. graph.markDirtyIfNeeded(file.string, fileIndex)
  904. of ideSug:
  905. # ideSug performs partial build of the file, thus mark it dirty for the
  906. # future calls.
  907. graph.markDirtyIfNeeded(file.string, fileIndex)
  908. of ideCon:
  909. graph.markDirty fileIndex
  910. graph.markClientsDirty fileIndex
  911. of ideOutline:
  912. let n = parseFile(fileIndex, graph.cache, graph.config)
  913. graph.iterateOutlineNodes(n, graph.fileSymbols(fileIndex).deduplicateSymInfoPair)
  914. of ideChk:
  915. myLog fmt "Reporting errors for {graph.suggestErrors.len} file(s)"
  916. for sug in graph.suggestErrorsIter:
  917. suggestResult(graph.config, sug)
  918. of ideChkFile:
  919. let errors = graph.suggestErrors.getOrDefault(fileIndex, @[])
  920. myLog fmt "Reporting {errors.len} error(s) for {file.string}"
  921. for error in errors:
  922. suggestResult(graph.config, error)
  923. of ideGlobalSymbols:
  924. var
  925. counter = 0
  926. res: seq[SymInfoPair] = @[]
  927. for s in graph.suggestSymbolsIter:
  928. if (sfGlobal in s.sym.flags or s.sym.kind in searchableSymKinds) and
  929. s.sym.info == s.info:
  930. if contains(s.sym.name.s, file.string):
  931. inc counter
  932. res = res.filterIt(not it.info.exactEquals(s.info))
  933. res.add s
  934. # stop after first 1000 matches...
  935. if counter > 1000:
  936. break
  937. # ... then sort them by weight ...
  938. res.sort() do (left, right: SymInfoPair) -> int:
  939. let
  940. leftString = left.sym.name.s
  941. rightString = right.sym.name.s
  942. leftIndex = leftString.find(file.string)
  943. rightIndex = rightString.find(file.string)
  944. if leftIndex == rightIndex:
  945. result = cmp(toLowerAscii(leftString),
  946. toLowerAscii(rightString))
  947. else:
  948. result = cmp(leftIndex, rightIndex)
  949. # ... and send first 100 results
  950. if res.len > 0:
  951. for i in 0 .. min(100, res.len - 1):
  952. let s = res[i]
  953. graph.suggestResult(s.sym, s.info)
  954. of ideDeclaration:
  955. let s = graph.findSymData(file, line, col)
  956. if not s.isNil:
  957. # find first mention of the symbol in the file containing the definition.
  958. # It is either the definition or the declaration.
  959. var first: SymInfoPair
  960. for symbol in graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair:
  961. if s.sym.symbolEqual(symbol.sym):
  962. first = symbol
  963. break
  964. if s.info.exactEquals(first.info):
  965. # we are on declaration, go to definition
  966. graph.suggestResult(first.sym, first.sym.info, ideDeclaration)
  967. else:
  968. # we are on definition or usage, look for declaration
  969. graph.suggestResult(first.sym, first.info, ideDeclaration)
  970. of ideExpand:
  971. var level: int = high(int)
  972. let index = skipWhitespace(tag, 0);
  973. let trimmed = substr(tag, index)
  974. if not (trimmed == "" or trimmed == "all"):
  975. discard parseInt(trimmed, level, 0)
  976. conf.expandPosition = newLineInfo(fileIndex, line, col)
  977. conf.expandLevels = level
  978. conf.expandProgress = false
  979. conf.expandNodeResult = ""
  980. graph.markDirty fileIndex
  981. graph.markClientsDirty fileIndex
  982. graph.recompilePartially()
  983. var suggest = Suggest()
  984. suggest.section = ideExpand
  985. suggest.version = 3
  986. suggest.line = line
  987. suggest.column = col
  988. suggest.doc = graph.config.expandNodeResult
  989. if suggest.doc != "":
  990. let
  991. n = parseFile(fileIndex, graph.cache, graph.config)
  992. endInfo = n.calculateExpandRange(conf.expandPosition)
  993. suggest.endLine = endInfo.line
  994. suggest.endCol = endInfo.col
  995. suggestResult(graph.config, suggest)
  996. graph.markDirty fileIndex
  997. graph.markClientsDirty fileIndex
  998. of ideInlayHints:
  999. myLog fmt "Executing inlayHints"
  1000. var endLine = 0
  1001. var endCol = -1
  1002. var i = 0
  1003. i += skipWhile(tag, seps, i)
  1004. i += parseInt(tag, endLine, i)
  1005. i += skipWhile(tag, seps, i)
  1006. i += parseInt(tag, endCol, i)
  1007. let s = graph.findSymDataInRange(file, line, col, endLine, endCol)
  1008. for q in s:
  1009. if q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType:
  1010. graph.suggestInlayHintResult(q.sym, q.info, ideInlayHints)
  1011. else:
  1012. myLog fmt "Discarding {cmd}"
  1013. # v3 end
  1014. when isMainModule:
  1015. handleCmdLine(newIdentCache(), newConfigRef())
  1016. else:
  1017. export Suggest
  1018. export IdeCmd
  1019. export AbsoluteFile
  1020. type NimSuggest* = ref object
  1021. graph: ModuleGraph
  1022. idle: int
  1023. cachedMsgs: CachedMsgs
  1024. proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
  1025. var retval: ModuleGraph
  1026. proc mockCommand(graph: ModuleGraph) =
  1027. retval = graph
  1028. let conf = graph.config
  1029. conf.setCmd cmdIdeTools
  1030. defineSymbol(conf.symbols, $conf.backend)
  1031. clearPasses(graph)
  1032. registerPass graph, verbosePass
  1033. registerPass graph, semPass
  1034. wantMainModule(conf)
  1035. if not fileExists(conf.projectFull):
  1036. quit "cannot find file: " & conf.projectFull.string
  1037. add(conf.searchPaths, conf.libpath)
  1038. conf.setErrorMaxHighMaybe
  1039. # do not print errors, but log them
  1040. conf.writelnHook = myLog
  1041. conf.structuredErrorHook = nil
  1042. # compile the project before showing any input so that we already
  1043. # can answer questions right away:
  1044. compileProject(graph)
  1045. proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
  1046. conf.suggestVersion = 0
  1047. let a = unixToNativePath(project)
  1048. if dirExists(a) and not fileExists(a.addFileExt("nim")):
  1049. conf.projectName = findProjectNimFile(conf, a)
  1050. # don't make it worse, report the error the old way:
  1051. if conf.projectName.len == 0: conf.projectName = a
  1052. else:
  1053. conf.projectName = a
  1054. # if processArgument(pass, p, argsCount): break
  1055. let
  1056. cache = newIdentCache()
  1057. conf = newConfigRef()
  1058. self = NimProg(
  1059. suggestMode: true,
  1060. processCmdLine: mockCmdLine
  1061. )
  1062. self.initDefinesProg(conf, "nimsuggest")
  1063. self.processCmdLineAndProjectPath(conf)
  1064. if gMode != mstdin:
  1065. conf.writelnHook = proc (msg: string) = discard
  1066. # Find Nim's prefix dir.
  1067. if nimPath == "":
  1068. let binaryPath = findExe("nim")
  1069. if binaryPath == "":
  1070. raise newException(IOError,
  1071. "Cannot find Nim standard library: Nim compiler not in PATH")
  1072. conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
  1073. if not dirExists(conf.prefixDir / RelativeDir"lib"):
  1074. conf.prefixDir = AbsoluteDir""
  1075. else:
  1076. conf.prefixDir = AbsoluteDir nimPath
  1077. #msgs.writelnHook = proc (line: string) = log(line)
  1078. myLog("START " & conf.projectFull.string)
  1079. var graph = newModuleGraph(cache, conf)
  1080. if self.loadConfigsAndProcessCmdLine(cache, conf, graph):
  1081. mockCommand(graph)
  1082. if gLogging:
  1083. myLog("Search paths:")
  1084. for it in conf.searchPaths:
  1085. myLog(" " & it.string)
  1086. retval.doStopCompile = proc (): bool = false
  1087. return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
  1088. proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
  1089. var retval: seq[Suggest] = @[]
  1090. let conf = nimsuggest.graph.config
  1091. conf.ideCmd = cmd
  1092. conf.writelnHook = proc (line: string) =
  1093. retval.add(Suggest(section: ideMsg, doc: line))
  1094. conf.suggestionResultHook = proc (s: Suggest) =
  1095. retval.add(s)
  1096. conf.writelnHook = proc (s: string) =
  1097. stderr.write s & "\n"
  1098. if conf.ideCmd == ideKnown:
  1099. retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
  1100. elif conf.ideCmd == ideProject:
  1101. retval.add(Suggest(section: ideProject, filePath: string conf.projectFull))
  1102. else:
  1103. if conf.ideCmd == ideChk:
  1104. for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
  1105. if conf.ideCmd == ideChk:
  1106. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  1107. retval.add(Suggest(section: ideChk, filePath: toFullPath(conf, info),
  1108. line: toLinenumber(info), column: toColumn(info), doc: msg,
  1109. forth: $sev))
  1110. else:
  1111. conf.structuredErrorHook = nil
  1112. executeNoHooks(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph)
  1113. return retval