nimsuggest.nim 47 KB

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