nimsuggest.nim 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  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): 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. fileIndex: xs.fileIndex,
  734. trackCaughtExceptions: xs.trackCaughtExceptions,
  735. isSorted: false
  736. )
  737. var i = xs.lineInfo.high
  738. while i >= 0:
  739. let itm = xs.lineInfo[i]
  740. var found = false
  741. for res in result.lineInfo:
  742. if res.exactEquals(itm):
  743. found = true
  744. break
  745. if not found:
  746. result.add(xs.getSymInfoPair(i))
  747. dec i
  748. result.reverse()
  749. proc findSymData(graph: ModuleGraph, trackPos: TLineInfo):
  750. ref SymInfoPair =
  751. let db = graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair
  752. doAssert(db.fileIndex == trackPos.fileIndex)
  753. for i in db.lineInfo.low..db.lineInfo.high:
  754. if isTracked(db.lineInfo[i], TinyLineInfo(line: trackPos.line, col: trackPos.col), db.sym[i].name.s.len):
  755. var res = db.getSymInfoPair(i)
  756. new(result)
  757. result[] = res
  758. break
  759. func isInRange*(current, startPos, endPos: TinyLineInfo, tokenLen: int): bool =
  760. result =
  761. (current.line > startPos.line or (current.line == startPos.line and current.col>=startPos.col)) and
  762. (current.line < endPos.line or (current.line == endPos.line and current.col <= endPos.col))
  763. proc findSymDataInRange(graph: ModuleGraph, startPos, endPos: TLineInfo):
  764. seq[SymInfoPair] =
  765. result = newSeq[SymInfoPair]()
  766. let db = graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair
  767. for i in db.lineInfo.low..db.lineInfo.high:
  768. 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):
  769. result.add(db.getSymInfoPair(i))
  770. proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
  771. ref SymInfoPair =
  772. let
  773. fileIdx = fileInfoIdx(graph.config, file)
  774. trackPos = newLineInfo(fileIdx, line, col)
  775. result = findSymData(graph, trackPos)
  776. proc findSymDataInRange(graph: ModuleGraph, file: AbsoluteFile; startLine, startCol, endLine, endCol: int):
  777. seq[SymInfoPair] =
  778. let
  779. fileIdx = fileInfoIdx(graph.config, file)
  780. startPos = newLineInfo(fileIdx, startLine, startCol)
  781. endPos = newLineInfo(fileIdx, endLine, endCol)
  782. result = findSymDataInRange(graph, startPos, endPos)
  783. proc markDirtyIfNeeded(graph: ModuleGraph, file: string, originalFileIdx: FileIndex) =
  784. let sha = $sha1.secureHashFile(file)
  785. if graph.config.m.fileInfos[originalFileIdx.int32].hash != sha or graph.config.ideCmd in {ideSug, ideCon}:
  786. myLog fmt "{file} changed compared to last compilation"
  787. graph.markDirty originalFileIdx
  788. graph.markClientsDirty originalFileIdx
  789. else:
  790. myLog fmt "No changes in file {file} compared to last compilation"
  791. proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
  792. defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
  793. let section = if defaultSection != ideNone:
  794. defaultSection
  795. elif sym.info.exactEquals(info):
  796. ideDef
  797. else:
  798. ideUse
  799. let suggest = symToSuggest(graph, sym, isLocal=false, section,
  800. info, 100, PrefixMatch.None, false, 0,
  801. endLine = endLine, endCol = endCol)
  802. suggestResult(graph.config, suggest)
  803. proc suggestInlayHintResultType(graph: ModuleGraph, sym: PSym, info: TLineInfo,
  804. defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
  805. let section = if defaultSection != ideNone:
  806. defaultSection
  807. elif sym.info.exactEquals(info):
  808. ideDef
  809. else:
  810. ideUse
  811. var suggestDef = symToSuggest(graph, sym, isLocal=false, section,
  812. info, 100, PrefixMatch.None, false, 0, true,
  813. endLine = endLine, endCol = endCol)
  814. suggestDef.inlayHintInfo = suggestToSuggestInlayTypeHint(suggestDef)
  815. suggestDef.section = ideInlayHints
  816. if sym.kind == skForVar:
  817. suggestDef.inlayHintInfo.allowInsert = false
  818. suggestResult(graph.config, suggestDef)
  819. proc suggestInlayHintResultException(graph: ModuleGraph, sym: PSym, info: TLineInfo,
  820. defaultSection = ideNone, caughtExceptions: seq[PType], caughtExceptionsSet: bool, endLine: uint16 = 0, endCol = 0) =
  821. if not caughtExceptionsSet:
  822. return
  823. if sym.kind == skParam and sfEffectsDelayed in sym.flags:
  824. return
  825. var raisesList: seq[PType] = @[getEbase(graph, info)]
  826. let t = sym.typ
  827. if not isNil(t) and not isNil(t.n) and t.n.len > 0 and t.n[0].len > exceptionEffects:
  828. let effects = t.n[0]
  829. if effects.kind == nkEffectList and effects.len == effectListLen:
  830. let effs = effects[exceptionEffects]
  831. if not isNil(effs):
  832. raisesList = @[]
  833. for eff in items(effs):
  834. if not isNil(eff):
  835. raisesList.add(eff.typ)
  836. var propagatedExceptionList: seq[PType] = @[]
  837. for re in raisesList:
  838. var exceptionIsPropagated = true
  839. for ce in caughtExceptions:
  840. if isNil(ce) or safeInheritanceDiff(re, ce) <= 0:
  841. exceptionIsPropagated = false
  842. break
  843. if exceptionIsPropagated:
  844. propagatedExceptionList.add(re)
  845. if propagatedExceptionList.len == 0:
  846. return
  847. let section = if defaultSection != ideNone:
  848. defaultSection
  849. elif sym.info.exactEquals(info):
  850. ideDef
  851. else:
  852. ideUse
  853. var suggestDef = symToSuggest(graph, sym, isLocal=false, section,
  854. info, 100, PrefixMatch.None, false, 0, true,
  855. endLine = endLine, endCol = endCol)
  856. suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintLeft(suggestDef, propagatedExceptionList)
  857. suggestDef.section = ideInlayHints
  858. suggestResult(graph.config, suggestDef)
  859. suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintRight(suggestDef, propagatedExceptionList)
  860. suggestResult(graph.config, suggestDef)
  861. const
  862. # kinds for ideOutline and ideGlobalSymbols
  863. searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate}
  864. proc symbolEqual(left, right: PSym): bool =
  865. # More relaxed symbol comparison
  866. return left.info.exactEquals(right.info) and left.name == right.name
  867. proc findDef(n: PNode, line: uint16, col: int16): PNode =
  868. if n.kind in {nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkMacroDef}:
  869. if n.info.line == line:
  870. return n
  871. else:
  872. for i in 0 ..< safeLen(n):
  873. let res = findDef(n[i], line, col)
  874. if res != nil: return res
  875. proc findByTLineInfo(trackPos: TLineInfo, infoPairs: SuggestFileSymbolDatabase):
  876. ref SymInfoPair =
  877. result = nil
  878. if infoPairs.fileIndex == trackPos.fileIndex:
  879. for i in infoPairs.lineInfo.low..infoPairs.lineInfo.high:
  880. let s = infoPairs.getSymInfoPair(i)
  881. if s.info.exactEquals trackPos:
  882. new(result)
  883. result[] = s
  884. break
  885. proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool =
  886. proc checkSymbol(sym: PSym, info: TLineInfo): bool =
  887. result = (sym.owner.kind in {skModule, skType} or sym.kind in {skProc, skMethod, skIterator, skTemplate, skType})
  888. if n.kind == nkSym and n.sym.checkSymbol(n.info):
  889. graph.suggestResult(n.sym, n.sym.info, ideOutline, endInfo.line, endInfo.col)
  890. return true
  891. elif n.kind == nkIdent:
  892. let symData = findByTLineInfo(n.info, infoPairs)
  893. if symData != nil and symData.sym.checkSymbol(symData.info):
  894. let sym = symData.sym
  895. graph.suggestResult(sym, sym.info, ideOutline, endInfo.line, endInfo.col)
  896. return true
  897. proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool =
  898. for child in n:
  899. if child.kind in {nkIdent, nkSym}:
  900. if graph.outlineNode(child, endInfo, infoPairs):
  901. return true
  902. elif child.kind == nkPostfix:
  903. if graph.handleIdentOrSym(child, endInfo, infoPairs):
  904. return true
  905. proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: SuggestFileSymbolDatabase) =
  906. var matched = true
  907. if n.kind == nkIdent:
  908. let symData = findByTLineInfo(n.info, infoPairs)
  909. if symData != nil and symData.sym.kind == skEnumField and symData.info.exactEquals(symData.sym.info):
  910. let sym = symData.sym
  911. graph.suggestResult(sym, sym.info, ideOutline, n.endInfo.line, n.endInfo.col)
  912. elif (n.kind in {nkFuncDef, nkProcDef, nkTypeDef, nkMacroDef, nkTemplateDef, nkConverterDef, nkEnumFieldDef, nkConstDef}):
  913. matched = handleIdentOrSym(graph, n, n.endInfo, infoPairs)
  914. else:
  915. matched = false
  916. if n.kind != nkFormalParams:
  917. for child in n:
  918. graph.iterateOutlineNodes(child, infoPairs)
  919. proc calculateExpandRange(n: PNode, info: TLineInfo): TLineInfo =
  920. if ((n.kind in {nkFuncDef, nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkConverterDef} and
  921. n.info.exactEquals(info)) or
  922. (n.kind in {nkCall, nkCommand} and n[0].info.exactEquals(info))):
  923. result = n.endInfo
  924. else:
  925. for child in n:
  926. result = child.calculateExpandRange(info)
  927. if result != unknownLineInfo:
  928. return result
  929. result = unknownLineInfo
  930. proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string,
  931. graph: ModuleGraph) =
  932. let conf = graph.config
  933. conf.writelnHook = proc (s: string) = discard
  934. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo;
  935. msg: string; sev: Severity) =
  936. let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info),
  937. line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev)
  938. graph.suggestErrors.mgetOrPut(info.fileIndex, @[]).add suggest
  939. conf.ideCmd = cmd
  940. myLog fmt "cmd: {cmd}, file: {file}[{line}:{col}], dirtyFile: {dirtyfile}, tag: {tag}"
  941. var fileIndex: FileIndex
  942. if not (cmd in {ideRecompile, ideGlobalSymbols}):
  943. fileIndex = fileInfoIdx(conf, file)
  944. msgs.setDirtyFile(
  945. conf,
  946. fileIndex,
  947. if dirtyfile.isEmpty: AbsoluteFile"" else: dirtyfile)
  948. if not dirtyfile.isEmpty:
  949. graph.markDirtyIfNeeded(dirtyFile.string, fileInfoIdx(conf, file))
  950. # these commands require fully compiled project
  951. if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk, ideInlayHints} and graph.needsCompilation():
  952. graph.recompilePartially()
  953. # when doing incremental build for the project root we should make sure that
  954. # everything is unmarked as no longer beeing dirty in case there is no
  955. # longer reference to a particular module. E. g. A depends on B, B is marked
  956. # as dirty and A loses B import.
  957. graph.unmarkAllDirty()
  958. # these commands require partially compiled project
  959. elif cmd in {ideSug, ideCon, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration, ideExpand} and
  960. (graph.needsCompilation(fileIndex) or cmd in {ideSug, ideCon}):
  961. # for ideSug use v2 implementation
  962. if cmd in {ideSug, ideCon}:
  963. conf.m.trackPos = newLineInfo(fileIndex, line, col)
  964. conf.m.trackPosAttached = false
  965. else:
  966. conf.m.trackPos = default(TLineInfo)
  967. graph.recompilePartially(fileIndex)
  968. case cmd
  969. of ideDef:
  970. let s = graph.findSymData(file, line, col)
  971. if not s.isNil:
  972. graph.suggestResult(s.sym, s.sym.info)
  973. of ideType:
  974. let s = graph.findSymData(file, line, col)
  975. if not s.isNil:
  976. let typeSym = s.sym.typ.sym
  977. if typeSym != nil:
  978. graph.suggestResult(typeSym, typeSym.info, ideType)
  979. elif s.sym.typ.len != 0:
  980. let genericType = s.sym.typ[0].sym
  981. graph.suggestResult(genericType, genericType.info, ideType)
  982. of ideUse, ideDus:
  983. let symbol = graph.findSymData(file, line, col)
  984. if not symbol.isNil:
  985. var res: seq[SymInfoPair] = @[]
  986. for s in graph.suggestSymbolsIter:
  987. if s.sym.symbolEqual(symbol.sym):
  988. res.add(s)
  989. for s in res.deduplicateSymInfoPair():
  990. graph.suggestResult(s.sym, s.info)
  991. of ideHighlight:
  992. let sym = graph.findSymData(file, line, col)
  993. if not sym.isNil:
  994. let fs = graph.fileSymbols(fileIndex)
  995. var usages: seq[SymInfoPair] = @[]
  996. for i in fs.lineInfo.low..fs.lineInfo.high:
  997. if fs.sym[i] == sym.sym:
  998. usages.add(fs.getSymInfoPair(i))
  999. myLog fmt "Found {usages.len} usages in {file.string}"
  1000. for s in usages:
  1001. graph.suggestResult(s.sym, s.info)
  1002. of ideRecompile:
  1003. graph.recompileFullProject()
  1004. of ideChanged:
  1005. graph.markDirtyIfNeeded(file.string, fileIndex)
  1006. of ideSug, ideCon:
  1007. # ideSug/ideCon performs partial build of the file, thus mark it dirty for the
  1008. # future calls.
  1009. graph.markDirtyIfNeeded(file.string, fileIndex)
  1010. graph.recompilePartially(fileIndex)
  1011. let m = graph.getModule fileIndex
  1012. incl m.flags, sfDirty
  1013. of ideOutline:
  1014. let n = parseFile(fileIndex, graph.cache, graph.config)
  1015. graph.iterateOutlineNodes(n, graph.fileSymbols(fileIndex).deduplicateSymInfoPair)
  1016. of ideChk:
  1017. myLog fmt "Reporting errors for {graph.suggestErrors.len} file(s)"
  1018. for sug in graph.suggestErrorsIter:
  1019. suggestResult(graph.config, sug)
  1020. of ideChkFile:
  1021. let errors = graph.suggestErrors.getOrDefault(fileIndex, @[])
  1022. myLog fmt "Reporting {errors.len} error(s) for {file.string}"
  1023. for error in errors:
  1024. suggestResult(graph.config, error)
  1025. of ideGlobalSymbols:
  1026. var
  1027. counter = 0
  1028. res: seq[SymInfoPair] = @[]
  1029. for s in graph.suggestSymbolsIter:
  1030. if (sfGlobal in s.sym.flags or s.sym.kind in searchableSymKinds) and
  1031. s.sym.info == s.info:
  1032. if contains(s.sym.name.s, file.string):
  1033. inc counter
  1034. res = res.filterIt(not it.info.exactEquals(s.info))
  1035. res.add s
  1036. # stop after first 1000 matches...
  1037. if counter > 1000:
  1038. break
  1039. # ... then sort them by weight ...
  1040. res.sort() do (left, right: SymInfoPair) -> int:
  1041. let
  1042. leftString = left.sym.name.s
  1043. rightString = right.sym.name.s
  1044. leftIndex = leftString.find(file.string)
  1045. rightIndex = rightString.find(file.string)
  1046. if leftIndex == rightIndex:
  1047. result = cmp(toLowerAscii(leftString),
  1048. toLowerAscii(rightString))
  1049. else:
  1050. result = cmp(leftIndex, rightIndex)
  1051. # ... and send first 100 results
  1052. if res.len > 0:
  1053. for i in 0 .. min(100, res.len - 1):
  1054. let s = res[i]
  1055. graph.suggestResult(s.sym, s.info)
  1056. of ideDeclaration:
  1057. let s = graph.findSymData(file, line, col)
  1058. if not s.isNil:
  1059. # find first mention of the symbol in the file containing the definition.
  1060. # It is either the definition or the declaration.
  1061. var first: SymInfoPair
  1062. let db = graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair
  1063. for i in db.lineInfo.low..db.lineInfo.high:
  1064. if s.sym.symbolEqual(db.sym[i]):
  1065. first = db.getSymInfoPair(i)
  1066. break
  1067. if s.info.exactEquals(first.info):
  1068. # we are on declaration, go to definition
  1069. graph.suggestResult(first.sym, first.sym.info, ideDeclaration)
  1070. else:
  1071. # we are on definition or usage, look for declaration
  1072. graph.suggestResult(first.sym, first.info, ideDeclaration)
  1073. of ideExpand:
  1074. var level: int = high(int)
  1075. let index = skipWhitespace(tag, 0);
  1076. let trimmed = substr(tag, index)
  1077. if not (trimmed == "" or trimmed == "all"):
  1078. discard parseInt(trimmed, level, 0)
  1079. conf.expandPosition = newLineInfo(fileIndex, line, col)
  1080. conf.expandLevels = level
  1081. conf.expandProgress = false
  1082. conf.expandNodeResult = ""
  1083. graph.markDirty fileIndex
  1084. graph.markClientsDirty fileIndex
  1085. graph.recompilePartially()
  1086. var suggest = Suggest()
  1087. suggest.section = ideExpand
  1088. suggest.version = 3
  1089. suggest.line = line
  1090. suggest.column = col
  1091. suggest.doc = graph.config.expandNodeResult
  1092. if suggest.doc != "":
  1093. let
  1094. n = parseFile(fileIndex, graph.cache, graph.config)
  1095. endInfo = n.calculateExpandRange(conf.expandPosition)
  1096. suggest.endLine = endInfo.line
  1097. suggest.endCol = endInfo.col
  1098. suggestResult(graph.config, suggest)
  1099. graph.markDirty fileIndex
  1100. graph.markClientsDirty fileIndex
  1101. of ideInlayHints:
  1102. myLog fmt "Executing inlayHints"
  1103. var endLine = 0
  1104. var endCol = -1
  1105. var i = 0
  1106. i += skipWhile(tag, seps, i)
  1107. i += parseInt(tag, endLine, i)
  1108. i += skipWhile(tag, seps, i)
  1109. i += parseInt(tag, endCol, i)
  1110. i += skipWhile(tag, seps, i)
  1111. var typeHints = true
  1112. var exceptionHints = false
  1113. while i <= tag.high:
  1114. var token: string
  1115. i += parseUntil(tag, token, seps, i)
  1116. i += skipWhile(tag, seps, i)
  1117. case token:
  1118. of "+typeHints":
  1119. typeHints = true
  1120. of "-typeHints":
  1121. typeHints = false
  1122. of "+exceptionHints":
  1123. exceptionHints = true
  1124. of "-exceptionHints":
  1125. exceptionHints = false
  1126. else:
  1127. myLog fmt "Discarding unknown inlay hint parameter {token}"
  1128. let s = graph.findSymDataInRange(file, line, col, endLine, endCol)
  1129. for q in s:
  1130. if typeHints and q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType:
  1131. graph.suggestInlayHintResultType(q.sym, q.info, ideInlayHints)
  1132. if exceptionHints and q.sym.kind in {skProc, skFunc, skMethod, skVar, skLet, skParam} and not q.isDecl:
  1133. graph.suggestInlayHintResultException(q.sym, q.info, ideInlayHints, caughtExceptions = q.caughtExceptions, caughtExceptionsSet = q.caughtExceptionsSet)
  1134. else:
  1135. myLog fmt "Discarding {cmd}"
  1136. # v3 end
  1137. when isMainModule:
  1138. handleCmdLine(newIdentCache(), newConfigRef())
  1139. else:
  1140. export Suggest
  1141. export IdeCmd
  1142. export AbsoluteFile
  1143. type NimSuggest* = ref object
  1144. graph: ModuleGraph
  1145. idle: int
  1146. cachedMsgs: CachedMsgs
  1147. proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
  1148. var retval: ModuleGraph
  1149. proc mockCommand(graph: ModuleGraph) =
  1150. retval = graph
  1151. let conf = graph.config
  1152. conf.setCmd cmdIdeTools
  1153. defineSymbol(conf.symbols, $conf.backend)
  1154. clearPasses(graph)
  1155. registerPass graph, verbosePass
  1156. registerPass graph, semPass
  1157. wantMainModule(conf)
  1158. if not fileExists(conf.projectFull):
  1159. quit "cannot find file: " & conf.projectFull.string
  1160. add(conf.searchPaths, conf.libpath)
  1161. conf.setErrorMaxHighMaybe
  1162. # do not print errors, but log them
  1163. conf.writelnHook = myLog
  1164. conf.structuredErrorHook = nil
  1165. # compile the project before showing any input so that we already
  1166. # can answer questions right away:
  1167. compileProject(graph)
  1168. proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
  1169. conf.suggestVersion = 0
  1170. let a = unixToNativePath(project)
  1171. if dirExists(a) and not fileExists(a.addFileExt("nim")):
  1172. conf.projectName = findProjectNimFile(conf, a)
  1173. # don't make it worse, report the error the old way:
  1174. if conf.projectName.len == 0: conf.projectName = a
  1175. else:
  1176. conf.projectName = a
  1177. # if processArgument(pass, p, argsCount): break
  1178. let
  1179. cache = newIdentCache()
  1180. conf = newConfigRef()
  1181. self = NimProg(
  1182. suggestMode: true,
  1183. processCmdLine: mockCmdLine
  1184. )
  1185. self.initDefinesProg(conf, "nimsuggest")
  1186. self.processCmdLineAndProjectPath(conf)
  1187. if gMode != mstdin:
  1188. conf.writelnHook = proc (msg: string) = discard
  1189. # Find Nim's prefix dir.
  1190. if nimPath == "":
  1191. conf.prefixDir = conf.getPrefixDir()
  1192. else:
  1193. conf.prefixDir = AbsoluteDir nimPath
  1194. #msgs.writelnHook = proc (line: string) = log(line)
  1195. myLog("START " & conf.projectFull.string)
  1196. var graph = newModuleGraph(cache, conf)
  1197. if self.loadConfigsAndProcessCmdLine(cache, conf, graph):
  1198. mockCommand(graph)
  1199. if gLogging:
  1200. myLog("Search paths:")
  1201. for it in conf.searchPaths:
  1202. myLog(" " & it.string)
  1203. retval.doStopCompile = proc (): bool = false
  1204. return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
  1205. proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
  1206. var retval: seq[Suggest] = @[]
  1207. let conf = nimsuggest.graph.config
  1208. conf.ideCmd = cmd
  1209. conf.writelnHook = proc (line: string) =
  1210. retval.add(Suggest(section: ideMsg, doc: line))
  1211. conf.suggestionResultHook = proc (s: Suggest) =
  1212. retval.add(s)
  1213. conf.writelnHook = proc (s: string) =
  1214. stderr.write s & "\n"
  1215. if conf.ideCmd == ideKnown:
  1216. retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
  1217. elif conf.ideCmd == ideProject:
  1218. retval.add(Suggest(section: ideProject, filePath: string conf.projectFull))
  1219. else:
  1220. if conf.ideCmd == ideChk:
  1221. for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
  1222. if conf.ideCmd == ideChk:
  1223. conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
  1224. retval.add(Suggest(section: ideChk, filePath: toFullPath(conf, info),
  1225. line: toLinenumber(info), column: toColumn(info), doc: msg,
  1226. forth: $sev))
  1227. else:
  1228. conf.structuredErrorHook = nil
  1229. executeNoHooks(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph)
  1230. return retval