nimsuggest.nim 47 KB

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