nimsuggest.nim 47 KB

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