diff options
Diffstat (limited to 'nimsuggest/nimsuggest.nim')
-rw-r--r-- | nimsuggest/nimsuggest.nim | 513 |
1 files changed, 427 insertions, 86 deletions
diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim index c4798e88c..04bae08c1 100644 --- a/nimsuggest/nimsuggest.nim +++ b/nimsuggest/nimsuggest.nim @@ -8,11 +8,22 @@ # import compiler/renderer +import compiler/types +import compiler/trees +import compiler/wordrecg +import compiler/sempass2 import strformat import algorithm import tables -import std/sha1 import times +import procmonitor + +template tryImport(module) = import module + +when compiles tryImport ../dist/checksums/src/checksums/sha1: + import ../dist/checksums/src/checksums/sha1 +else: + import checksums/sha1 ## Nimsuggest is a tool that helps to give editors IDE like capabilities. @@ -23,11 +34,11 @@ import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp # Do NOT import suggest. It will lead to weird bugs with # suggestionResultHook, because suggest.nim is included by sigmatch. # So we import that one instead. -import compiler / [options, commands, modules, sem, +import compiler / [options, commands, modules, passes, passaux, msgs, sigmatch, ast, idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper, - pathutils] + pathutils, condsyms, syntaxes, suggestsymdb] when defined(nimPreviewSlimSystem): import std/typedthreads @@ -37,6 +48,7 @@ when defined(windows): else: import posix +const HighestSuggestProtocolVersion = 4 const DummyEof = "!EOF!" const Usage = """ Nimsuggest - Tool to give every editor IDE like capabilities for Nim @@ -49,17 +61,25 @@ Options: --address:HOST binds to that address, by default "" --stdin read commands from stdin and write results to stdout instead of using sockets + --clientProcessId:PID shutdown nimsuggest in case this process dies --epc use emacs epc mode --debug enable debug output --log enable verbose logging to nimsuggest.log file --v1 use version 1 of the protocol; for backwards compatibility --v2 use version 2(default) of the protocol --v3 use version 3 of the protocol + --v4 use version 4 of the protocol + --info:X information + --info:nimVer return the Nim compiler version that nimsuggest uses internally + --info:protocolVer return the newest protocol version that is supported + --info:capabilities return the capabilities supported by nimsuggest --refresh perform automatic refreshes to keep the analysis precise --maxresults:N limit the number of suggestions to N --tester implies --stdin and outputs a line '""" & DummyEof & """' for the tester --find attempts to find the project file of the current project + --exceptionInlayHints:on|off + globally turn exception inlay hints on|off The server then listens to the connection and takes line-based commands. @@ -78,7 +98,7 @@ type var gPort = 6000.Port - gAddress = "" + gAddress = "127.0.0.1" gMode: Mode gEmitEof: bool # whether we write '!EOF!' dummy lines gLogging = defined(logging) @@ -88,7 +108,7 @@ var requests: Channel[string] results: Channel[Suggest] -proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; +proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string, graph: ModuleGraph); proc writelnToChannel(line: string) = @@ -111,6 +131,12 @@ const "type 'quit' to quit\n" & "type 'debug' to toggle debug mode on/off\n" & "type 'terse' to toggle terse mode on/off" + #List of currently supported capabilities. So lang servers/ides can iterate over and check for what's enabled + Capabilities = [ + "con", #current NimSuggest supports the `con` commmand + "exceptionInlayHints", + "unknownFile", #current NimSuggest can handle unknown files + ] proc parseQuoted(cmd: string; outp: var string; start: int): int = var i = start @@ -140,6 +166,9 @@ proc sexp(s: Suggest): SexpNode = ]) if s.section == ideSug: result.add convertSexp(s.prefix) + if s.section in {ideOutline, ideExpand} and s.version == 3: + result.add convertSexp(s.endLine.int) + result.add convertSexp(s.endCol) proc sexp(s: seq[Suggest]): SexpNode = result = newSList() @@ -152,7 +181,7 @@ proc listEpc(): SexpNode = argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol)) docstring = sexp("line starts at 1, column at 0, dirtyfile is optional") result = newSList() - for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration"]: + for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration", "inlayHints"]: let cmd = sexp(command) methodDesc = newSList() @@ -175,12 +204,49 @@ proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo): PSym = if m != nil and m.ast != nil: result = findNode(m.ast, trackPos) -proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; +template benchmark(benchmarkName: untyped, code: untyped) = + block: + myLog "Started [" & benchmarkName & "]..." + let t0 = epochTime() + code + let elapsed = epochTime() - t0 + let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 3) + myLog "CPU Time [" & benchmarkName & "] " & elapsedStr & "s" + +proc clearInstCache(graph: ModuleGraph, projectFileIdx: FileIndex) = + if projectFileIdx == InvalidFileIdx: + graph.typeInstCache.clear() + graph.procInstCache.clear() + return + var typeIdsToDelete = newSeq[ItemId]() + for id in graph.typeInstCache.keys: + if id.module == projectFileIdx.int: + typeIdsToDelete.add id + for id in typeIdsToDelete: + graph.typeInstCache.del id + var procIdsToDelete = newSeq[ItemId]() + for id in graph.procInstCache.keys: + if id.module == projectFileIdx.int: + procIdsToDelete.add id + for id in procIdsToDelete: + graph.procInstCache.del id + + for tbl in mitems(graph.attachedOps): + var attachedOpsToDelete = newSeq[ItemId]() + for id in tbl.keys: + if id.module == projectFileIdx.int and sfOverridden in resolveAttachedOp(graph, tbl[id]).flags: + attachedOpsToDelete.add id + for id in attachedOpsToDelete: + tbl.del id + +proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, tag: string, graph: ModuleGraph) = let conf = graph.config - if conf.suggestVersion == 3: - executeNoHooksV3(cmd, file, dirtyfile, line, col, graph) + if conf.suggestVersion >= 3: + let command = fmt "cmd = {cmd} {file}:{line}:{col}" + benchmark command: + executeNoHooksV3(cmd, file, dirtyfile, line, col, tag, graph) return myLog("cmd: " & $cmd & ", file: " & file.string & @@ -201,6 +267,7 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; if conf.suggestVersion == 1: graph.usageSym = nil if not isKnownFile: + graph.clearInstCache(dirtyIdx) graph.compileProject(dirtyIdx) if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and dirtyfile.isEmpty: @@ -211,6 +278,7 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; graph.markClientsDirty dirtyIdx if conf.ideCmd != ideMod: if isKnownFile: + graph.clearInstCache(modIdx) graph.compileProject(modIdx) if conf.ideCmd in {ideUse, ideDus}: let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym @@ -219,7 +287,10 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; else: localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos)) -proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; +proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int, graph: ModuleGraph) = + executeNoHooks(cmd, file, dirtyfile, line, col, "", graph) + +proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; tag: string, graph: ModuleGraph) = if cmd == ideChk: graph.config.structuredErrorHook = errorHook @@ -227,7 +298,7 @@ proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int; else: graph.config.structuredErrorHook = nil graph.config.writelnHook = myLog - executeNoHooks(cmd, file, dirtyfile, line, col, graph) + executeNoHooks(cmd, file, dirtyfile, line, col, tag, graph) proc executeEpc(cmd: IdeCmd, args: SexpNode; graph: ModuleGraph) = @@ -238,7 +309,7 @@ proc executeEpc(cmd: IdeCmd, args: SexpNode; var dirtyfile = AbsoluteFile"" if len(args) > 3: dirtyfile = AbsoluteFile args[3].getStr("") - execute(cmd, file, dirtyfile, int(line), int(column), graph) + execute(cmd, file, dirtyfile, int(line), int(column), args[3].getStr, graph) proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string, returnSymbol = "return") = @@ -459,9 +530,15 @@ proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) = of "changed": conf.ideCmd = ideChanged of "globalsymbols": conf.ideCmd = ideGlobalSymbols of "declaration": conf.ideCmd = ideDeclaration + of "expand": conf.ideCmd = ideExpand of "chkfile": conf.ideCmd = ideChkFile of "recompile": conf.ideCmd = ideRecompile of "type": conf.ideCmd = ideType + of "inlayhints": + if conf.suggestVersion >= 4: + conf.ideCmd = ideInlayHints + else: + err() else: err() var dirtyfile = "" var orig = "" @@ -478,6 +555,7 @@ proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) = i += parseInt(cmd, line, i) i += skipWhile(cmd, seps, i) i += parseInt(cmd, col, i) + let tag = substr(cmd, i) if conf.ideCmd == ideKnown: results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig)))) @@ -486,18 +564,9 @@ proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) = else: if conf.ideCmd == ideChk: for cm in cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev) - execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, graph) + execute(conf.ideCmd, AbsoluteFile orig, AbsoluteFile dirtyfile, line, col, tag, graph) sentinel() -template benchmark(benchmarkName: string, code: untyped) = - block: - myLog "Started [" & benchmarkName & "]..." - let t0 = epochTime() - code - let elapsed = epochTime() - t0 - let elapsedStr = elapsed.formatFloat(format = ffDecimal, precision = 3) - myLog "CPU Time [" & benchmarkName & "] " & elapsedStr & "s" - proc recompileFullProject(graph: ModuleGraph) = benchmark "Recompilation(clean)": graph.resetForBackend() @@ -509,9 +578,9 @@ proc recompileFullProject(graph: ModuleGraph) = proc mainThread(graph: ModuleGraph) = let conf = graph.config - if gLogging: - for it in conf.searchPaths: - log(it.string) + myLog "searchPaths: " + for it in conf.searchPaths: + myLog(" " & it.string) proc wrHook(line: string) {.closure.} = if gMode == mepc: @@ -534,7 +603,7 @@ proc mainThread(graph: ModuleGraph) = else: os.sleep 250 idle += 1 - if idle == 20 and gRefresh and conf.suggestVersion != 3: + if idle == 20 and gRefresh and conf.suggestVersion < 3: # we use some nimsuggest activity to enable a lazy recompile: conf.ideCmd = ideChk conf.writelnHook = proc (s: string) = discard @@ -553,6 +622,7 @@ proc mainCommand(graph: ModuleGraph) = registerPass graph, verbosePass registerPass graph, semPass conf.setCmd cmdIdeTools + defineSymbol(conf.symbols, $conf.backend) wantMainModule(conf) if not fileExists(conf.projectFull): @@ -564,7 +634,7 @@ proc mainCommand(graph: ModuleGraph) = # do not print errors, but log them conf.writelnHook = proc (msg: string) = discard - if graph.config.suggestVersion == 3: + if graph.config.suggestVersion >= 3: graph.config.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) = let suggest = Suggest(section: ideChk, filePath: toFullPath(conf, info), line: toLinenumber(info), column: toColumn(info), doc: msg, forth: $sev) @@ -578,6 +648,9 @@ proc mainCommand(graph: ModuleGraph) = open(requests) open(results) + if graph.config.clientProcessId != 0: + hookProcMonitor(graph.config.clientProcessId) + case gMode of mstdin: createThread(inputThread, replStdin, (gPort, gAddress)) of mtcp: createThread(inputThread, replTcp, (gPort, gAddress)) @@ -628,6 +701,25 @@ proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) = of "v1": conf.suggestVersion = 1 of "v2": conf.suggestVersion = 0 of "v3": conf.suggestVersion = 3 + of "v4": conf.suggestVersion = 4 + of "info": + case p.val.normalize + of "protocolver": + stdout.writeLine(HighestSuggestProtocolVersion) + quit 0 + of "nimver": + stdout.writeLine(system.NimVersion) + quit 0 + of "capabilities": + stdout.writeLine(Capabilities.toSeq.mapIt($it).join(" ")) + quit 0 + else: + processSwitch(pass, p, conf) + of "exceptioninlayhints": + case p.val.normalize + of "", "on": incl(conf.globalOptions, optIdeExceptionInlayHints) + of "off": excl(conf.globalOptions, optIdeExceptionInlayHints) + else: processSwitch(pass, p, conf) of "tester": gMode = mstdin gEmitEof = true @@ -642,6 +734,8 @@ proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) = conf.suggestMaxResults = parseInt(p.val) of "find": findProject = true + of "clientprocessid": + conf.clientProcessId = parseInt(p.val) else: processSwitch(pass, p, conf) of cmdArgument: let a = unixToNativePath(p.key) @@ -673,20 +767,16 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = if gMode != mstdin: conf.writelnHook = proc (msg: string) = discard - # Find Nim's prefix dir. - let binaryPath = findExe("nim") - if binaryPath == "": - raise newException(IOError, - "Cannot find Nim standard library: Nim compiler not in PATH") - conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir() - if not dirExists(conf.prefixDir / RelativeDir"lib"): - conf.prefixDir = AbsoluteDir"" - + conf.prefixDir = conf.getPrefixDir() #msgs.writelnHook = proc (line: string) = log(line) myLog("START " & conf.projectFull.string) var graph = newModuleGraph(cache, conf) if self.loadConfigsAndProcessCmdLine(cache, conf, graph): + + if conf.selectedGC == gcUnselected and + conf.backend != backendJs: + initOrcDefines(conf) mainCommand(graph) # v3 start @@ -699,9 +789,7 @@ proc recompilePartially(graph: ModuleGraph, projectFileIdx = InvalidFileIdx) = # inst caches are breaking incremental compilation when the cache caches stuff # from dirty buffer - # TODO: investigate more efficient way to achieve the same - # graph.typeInstCache.clear() - # graph.procInstCache.clear() + graph.clearInstCache(projectFileIdx) GC_fullCollect() @@ -720,7 +808,7 @@ func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair] # sym may not match. This can happen when xs contains the same definition but # with different signature because suggestSym might be called multiple times # for the same symbol (e. g. including/excluding the pragma) - result = @[] + result = newSeqOfCap[SymInfoPair](xs.len) for itm in xs.reversed: var found = false for res in result: @@ -731,27 +819,84 @@ func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair] result.add(itm) result.reverse() +func deduplicateSymInfoPair(xs: SuggestFileSymbolDatabase): SuggestFileSymbolDatabase = + # xs contains duplicate items and we want to filter them by range because the + # sym may not match. This can happen when xs contains the same definition but + # with different signature because suggestSym might be called multiple times + # for the same symbol (e. g. including/excluding the pragma) + result = SuggestFileSymbolDatabase( + lineInfo: newSeqOfCap[TinyLineInfo](xs.lineInfo.len), + sym: newSeqOfCap[PSym](xs.sym.len), + isDecl: newPackedBoolArray(), + caughtExceptions: newSeqOfCap[seq[PType]](xs.caughtExceptions.len), + caughtExceptionsSet: newPackedBoolArray(), + fileIndex: xs.fileIndex, + trackCaughtExceptions: xs.trackCaughtExceptions, + isSorted: false + ) + var i = xs.lineInfo.high + while i >= 0: + let itm = xs.lineInfo[i] + var found = false + for res in result.lineInfo: + if res.exactEquals(itm): + found = true + break + if not found: + result.add(xs.getSymInfoPair(i)) + dec i + result.reverse() + +proc findSymData(graph: ModuleGraph, trackPos: TLineInfo): + ref SymInfoPair = + let db = graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair + doAssert(db.fileIndex == trackPos.fileIndex) + for i in db.lineInfo.low..db.lineInfo.high: + if isTracked(db.lineInfo[i], TinyLineInfo(line: trackPos.line, col: trackPos.col), db.sym[i].name.s.len): + var res = db.getSymInfoPair(i) + new(result) + result[] = res + break + +func isInRange*(current, startPos, endPos: TinyLineInfo, tokenLen: int): bool = + result = + (current.line > startPos.line or (current.line == startPos.line and current.col>=startPos.col)) and + (current.line < endPos.line or (current.line == endPos.line and current.col <= endPos.col)) + +proc findSymDataInRange(graph: ModuleGraph, startPos, endPos: TLineInfo): + seq[SymInfoPair] = + result = newSeq[SymInfoPair]() + let db = graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair + for i in db.lineInfo.low..db.lineInfo.high: + 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): + result.add(db.getSymInfoPair(i)) + proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int): ref SymInfoPair = let fileIdx = fileInfoIdx(graph.config, file) trackPos = newLineInfo(fileIdx, line, col) - for s in graph.fileSymbols(fileIdx).deduplicateSymInfoPair: - if isTracked(s.info, trackPos, s.sym.name.s.len): - new(result) - result[] = s - break + result = findSymData(graph, trackPos) + +proc findSymDataInRange(graph: ModuleGraph, file: AbsoluteFile; startLine, startCol, endLine, endCol: int): + seq[SymInfoPair] = + let + fileIdx = fileInfoIdx(graph.config, file) + startPos = newLineInfo(fileIdx, startLine, startCol) + endPos = newLineInfo(fileIdx, endLine, endCol) + result = findSymDataInRange(graph, startPos, endPos) proc markDirtyIfNeeded(graph: ModuleGraph, file: string, originalFileIdx: FileIndex) = let sha = $sha1.secureHashFile(file) - if graph.config.m.fileInfos[originalFileIdx.int32].hash != sha or graph.config.ideCmd == ideSug: + if graph.config.m.fileInfos[originalFileIdx.int32].hash != sha or graph.config.ideCmd in {ideSug, ideCon}: myLog fmt "{file} changed compared to last compilation" graph.markDirty originalFileIdx graph.markClientsDirty originalFileIdx else: myLog fmt "No changes in file {file} compared to last compilation" -proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo, defaultSection = ideNone) = +proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) = let section = if defaultSection != ideNone: defaultSection elif sym.info.exactEquals(info): @@ -759,9 +904,76 @@ proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo, defaultSectio else: ideUse let suggest = symToSuggest(graph, sym, isLocal=false, section, - info, 100, PrefixMatch.None, false, 0) + info, 100, PrefixMatch.None, false, 0, + endLine = endLine, endCol = endCol) suggestResult(graph.config, suggest) +proc suggestInlayHintResultType(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) = + let section = if defaultSection != ideNone: + defaultSection + elif sym.info.exactEquals(info): + ideDef + else: + ideUse + var suggestDef = symToSuggest(graph, sym, isLocal=false, section, + info, 100, PrefixMatch.None, false, 0, true, + endLine = endLine, endCol = endCol) + suggestDef.inlayHintInfo = suggestToSuggestInlayTypeHint(suggestDef) + suggestDef.section = ideInlayHints + if sym.kind == skForVar: + suggestDef.inlayHintInfo.allowInsert = false + suggestResult(graph.config, suggestDef) + +proc suggestInlayHintResultException(graph: ModuleGraph, sym: PSym, info: TLineInfo, + defaultSection = ideNone, caughtExceptions: seq[PType], caughtExceptionsSet: bool, endLine: uint16 = 0, endCol = 0) = + if not caughtExceptionsSet: + return + + if sym.kind == skParam and sfEffectsDelayed in sym.flags: + return + + var raisesList: seq[PType] = @[getEbase(graph, info)] + + let t = sym.typ + if not isNil(t) and not isNil(t.n) and t.n.len > 0 and t.n[0].len > exceptionEffects: + let effects = t.n[0] + if effects.kind == nkEffectList and effects.len == effectListLen: + let effs = effects[exceptionEffects] + if not isNil(effs): + raisesList = @[] + for eff in items(effs): + if not isNil(eff): + raisesList.add(eff.typ) + + var propagatedExceptionList: seq[PType] = @[] + for re in raisesList: + var exceptionIsPropagated = true + for ce in caughtExceptions: + if isNil(ce) or safeInheritanceDiff(re, ce) <= 0: + exceptionIsPropagated = false + break + if exceptionIsPropagated: + propagatedExceptionList.add(re) + + if propagatedExceptionList.len == 0: + return + + let section = if defaultSection != ideNone: + defaultSection + elif sym.info.exactEquals(info): + ideDef + else: + ideUse + var suggestDef = symToSuggest(graph, sym, isLocal=false, section, + info, 100, PrefixMatch.None, false, 0, true, + endLine = endLine, endCol = endCol) + suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintLeft(suggestDef, propagatedExceptionList) + suggestDef.section = ideInlayHints + suggestResult(graph.config, suggestDef) + suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintRight(suggestDef, propagatedExceptionList) + suggestResult(graph.config, suggestDef) + const # kinds for ideOutline and ideGlobalSymbols searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate} @@ -770,7 +982,78 @@ proc symbolEqual(left, right: PSym): bool = # More relaxed symbol comparison return left.info.exactEquals(right.info) and left.name == right.name -proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; +proc findDef(n: PNode, line: uint16, col: int16): PNode = + if n.kind in {nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkMacroDef}: + if n.info.line == line: + return n + else: + for i in 0 ..< safeLen(n): + let res = findDef(n[i], line, col) + if res != nil: return res + +proc findByTLineInfo(trackPos: TLineInfo, infoPairs: SuggestFileSymbolDatabase): + ref SymInfoPair = + result = nil + if infoPairs.fileIndex == trackPos.fileIndex: + for i in infoPairs.lineInfo.low..infoPairs.lineInfo.high: + let s = infoPairs.getSymInfoPair(i) + if s.info.exactEquals trackPos: + new(result) + result[] = s + break + +proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool = + proc checkSymbol(sym: PSym, info: TLineInfo): bool = + result = (sym.owner.kind in {skModule, skType} or sym.kind in {skProc, skMethod, skIterator, skTemplate, skType}) + + if n.kind == nkSym and n.sym.checkSymbol(n.info): + graph.suggestResult(n.sym, n.sym.info, ideOutline, endInfo.line, endInfo.col) + return true + elif n.kind == nkIdent: + let symData = findByTLineInfo(n.info, infoPairs) + if symData != nil and symData.sym.checkSymbol(symData.info): + let sym = symData.sym + graph.suggestResult(sym, sym.info, ideOutline, endInfo.line, endInfo.col) + return true + +proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool = + for child in n: + if child.kind in {nkIdent, nkSym}: + if graph.outlineNode(child, endInfo, infoPairs): + return true + elif child.kind == nkPostfix: + if graph.handleIdentOrSym(child, endInfo, infoPairs): + return true + +proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: SuggestFileSymbolDatabase) = + var matched = true + if n.kind == nkIdent: + let symData = findByTLineInfo(n.info, infoPairs) + if symData != nil and symData.sym.kind == skEnumField and symData.info.exactEquals(symData.sym.info): + let sym = symData.sym + graph.suggestResult(sym, sym.info, ideOutline, n.endInfo.line, n.endInfo.col) + elif (n.kind in {nkFuncDef, nkProcDef, nkTypeDef, nkMacroDef, nkTemplateDef, nkConverterDef, nkEnumFieldDef, nkConstDef}): + matched = handleIdentOrSym(graph, n, n.endInfo, infoPairs) + else: + matched = false + + if n.kind != nkFormalParams: + for child in n: + graph.iterateOutlineNodes(child, infoPairs) + +proc calculateExpandRange(n: PNode, info: TLineInfo): TLineInfo = + if ((n.kind in {nkFuncDef, nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkConverterDef} and + n.info.exactEquals(info)) or + (n.kind in {nkCall, nkCommand} and n[0].info.exactEquals(info))): + result = n.endInfo + else: + for child in n: + result = child.calculateExpandRange(info) + if result != unknownLineInfo: + return result + result = unknownLineInfo + +proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int; tag: string, graph: ModuleGraph) = let conf = graph.config conf.writelnHook = proc (s: string) = discard @@ -782,15 +1065,11 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, conf.ideCmd = cmd - myLog fmt "cmd: {cmd}, file: {file}[{line}:{col}], dirtyFile: {dirtyfile}" + myLog fmt "cmd: {cmd}, file: {file}[{line}:{col}], dirtyFile: {dirtyfile}, tag: {tag}" var fileIndex: FileIndex if not (cmd in {ideRecompile, ideGlobalSymbols}): - if not fileInfoKnown(conf, file): - myLog fmt "{file} is unknown, returning no results" - return - fileIndex = fileInfoIdx(conf, file) msgs.setDirtyFile( conf, @@ -801,7 +1080,7 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, graph.markDirtyIfNeeded(dirtyFile.string, fileInfoIdx(conf, file)) # these commands require fully compiled project - if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk} and graph.needsCompilation(): + if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk, ideInlayHints} and graph.needsCompilation(): graph.recompilePartially() # when doing incremental build for the project root we should make sure that # everything is unmarked as no longer beeing dirty in case there is no @@ -810,16 +1089,15 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, graph.unmarkAllDirty() # these commands require partially compiled project - elif cmd in {ideSug, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration} and - (graph.needsCompilation(fileIndex) or cmd == ideSug): + elif cmd in {ideSug, ideCon, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration, ideExpand} and + (graph.needsCompilation(fileIndex) or cmd in {ideSug, ideCon}): # for ideSug use v2 implementation - if cmd == ideSug: + if cmd in {ideSug, ideCon}: conf.m.trackPos = newLineInfo(fileIndex, line, col) conf.m.trackPosAttached = false else: conf.m.trackPos = default(TLineInfo) - - graph.recompilePartially(fileIndex) + graph.recompilePartially(fileIndex) case cmd of ideDef: @@ -847,7 +1125,11 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, of ideHighlight: let sym = graph.findSymData(file, line, col) if not sym.isNil: - let usages = graph.fileSymbols(fileIndex).filterIt(it.sym == sym.sym) + let fs = graph.fileSymbols(fileIndex) + var usages: seq[SymInfoPair] = @[] + for i in fs.lineInfo.low..fs.lineInfo.high: + if fs.sym[i] == sym.sym: + usages.add(fs.getSymInfoPair(i)) myLog fmt "Found {usages.len} usages in {file.string}" for s in usages: graph.suggestResult(s.sym, s.info) @@ -855,21 +1137,16 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, graph.recompileFullProject() of ideChanged: graph.markDirtyIfNeeded(file.string, fileIndex) - of ideSug: - # ideSug performs partial build of the file, thus mark it dirty for the + of ideSug, ideCon: + # ideSug/ideCon performs partial build of the file, thus mark it dirty for the # future calls. graph.markDirtyIfNeeded(file.string, fileIndex) + graph.recompilePartially(fileIndex) + let m = graph.getModule fileIndex + incl m.flags, sfDirty of ideOutline: - let - module = graph.getModule fileIndex - symbols = graph.fileSymbols(fileIndex) - .deduplicateSymInfoPair - .filterIt(it.sym.info.exactEquals(it.info) and - (it.sym.owner == module or - it.sym.kind in searchableSymKinds)) - - for s in symbols: - graph.suggestResult(s.sym, s.info, ideOutline) + let n = parseFile(fileIndex, graph.cache, graph.config) + graph.iterateOutlineNodes(n, graph.fileSymbols(fileIndex).deduplicateSymInfoPair) of ideChk: myLog fmt "Reporting errors for {graph.suggestErrors.len} file(s)" for sug in graph.suggestErrorsIter: @@ -921,9 +1198,10 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, # find first mention of the symbol in the file containing the definition. # It is either the definition or the declaration. var first: SymInfoPair - for symbol in graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair: - if s.sym.symbolEqual(symbol.sym): - first = symbol + let db = graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair + for i in db.lineInfo.low..db.lineInfo.high: + if s.sym.symbolEqual(db.sym[i]): + first = db.getSymInfoPair(i) break if s.info.exactEquals(first.info): @@ -932,6 +1210,73 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, else: # we are on definition or usage, look for declaration graph.suggestResult(first.sym, first.info, ideDeclaration) + of ideExpand: + var level: int = high(int) + let index = skipWhitespace(tag, 0); + let trimmed = substr(tag, index) + if not (trimmed == "" or trimmed == "all"): + discard parseInt(trimmed, level, 0) + + conf.expandPosition = newLineInfo(fileIndex, line, col) + conf.expandLevels = level + conf.expandProgress = false + conf.expandNodeResult = "" + + graph.markDirty fileIndex + graph.markClientsDirty fileIndex + graph.recompilePartially() + var suggest = Suggest() + suggest.section = ideExpand + suggest.version = 3 + suggest.line = line + suggest.column = col + suggest.doc = graph.config.expandNodeResult + if suggest.doc != "": + let + n = parseFile(fileIndex, graph.cache, graph.config) + endInfo = n.calculateExpandRange(conf.expandPosition) + + suggest.endLine = endInfo.line + suggest.endCol = endInfo.col + + suggestResult(graph.config, suggest) + + graph.markDirty fileIndex + graph.markClientsDirty fileIndex + of ideInlayHints: + myLog fmt "Executing inlayHints" + var endLine = 0 + var endCol = -1 + var i = 0 + i += skipWhile(tag, seps, i) + i += parseInt(tag, endLine, i) + i += skipWhile(tag, seps, i) + i += parseInt(tag, endCol, i) + i += skipWhile(tag, seps, i) + var typeHints = true + var exceptionHints = false + while i <= tag.high: + var token: string + i += parseUntil(tag, token, seps, i) + i += skipWhile(tag, seps, i) + case token: + of "+typeHints": + typeHints = true + of "-typeHints": + typeHints = false + of "+exceptionHints": + exceptionHints = true + of "-exceptionHints": + exceptionHints = false + else: + myLog fmt "Discarding unknown inlay hint parameter {token}" + + let s = graph.findSymDataInRange(file, line, col, endLine, endCol) + for q in s: + if typeHints and q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType: + graph.suggestInlayHintResultType(q.sym, q.info, ideInlayHints) + if exceptionHints and q.sym.kind in {skProc, skFunc, skMethod, skVar, skLet, skParam} and not q.isDecl: + graph.suggestInlayHintResultException(q.sym, q.info, ideInlayHints, caughtExceptions = q.caughtExceptions, caughtExceptionsSet = q.caughtExceptionsSet) else: myLog fmt "Discarding {cmd}" @@ -952,10 +1297,12 @@ else: proc mockCommand(graph: ModuleGraph) = retval = graph let conf = graph.config + conf.setCmd cmdIdeTools + defineSymbol(conf.symbols, $conf.backend) clearPasses(graph) registerPass graph, verbosePass registerPass graph, semPass - conf.setCmd cmdIdeTools + wantMainModule(conf) if not fileExists(conf.projectFull): @@ -998,13 +1345,7 @@ else: conf.writelnHook = proc (msg: string) = discard # Find Nim's prefix dir. if nimPath == "": - let binaryPath = findExe("nim") - if binaryPath == "": - raise newException(IOError, - "Cannot find Nim standard library: Nim compiler not in PATH") - conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir() - if not dirExists(conf.prefixDir / RelativeDir"lib"): - conf.prefixDir = AbsoluteDir"" + conf.prefixDir = conf.getPrefixDir() else: conf.prefixDir = AbsoluteDir nimPath @@ -1015,9 +1356,9 @@ else: if self.loadConfigsAndProcessCmdLine(cache, conf, graph): mockCommand(graph) if gLogging: - log("Search paths:") + myLog("Search paths:") for it in conf.searchPaths: - log(" " & it.string) + myLog(" " & it.string) retval.doStopCompile = proc (): bool = false return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[]) |