nimsuggest.nim 47 KB

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