123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- #
- #
- # The Nim Compiler
- # (c) Copyright 2017 Andreas Rumpf
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## Nimsuggest is a tool that helps to give editors IDE like capabilities.
- when not defined(nimcore):
- {.error: "nimcore MUST be defined for Nim's core tooling".}
- import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp
- # Do NOT import suggest. It will lead to wierd bugs with
- # suggestionResultHook, because suggest.nim is included by sigmatch.
- # So we import that one instead.
- import compiler / [options, commands, modules, sem,
- passes, passaux, msgs, nimconf,
- extccomp, condsyms,
- sigmatch, ast, scriptconfig,
- idents, modulegraphs, vm, prefixmatches, lineinfos, cmdlinehelper,
- pathutils]
- when defined(windows):
- import winlean
- else:
- import posix
- const DummyEof = "!EOF!"
- const Usage = """
- Nimsuggest - Tool to give every editor IDE like capabilities for Nim
- Usage:
- nimsuggest [options] projectfile.nim
- Options:
- --port:PORT port, by default 6000
- --address:HOST binds to that address, by default ""
- --stdin read commands from stdin and write results to
- stdout instead of using sockets
- --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
- --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
- The server then listens to the connection and takes line-based commands.
- In addition, all command line options of Nim that do not affect code generation
- are supported.
- """
- type
- Mode = enum mstdin, mtcp, mepc, mcmdsug, mcmdcon
- CachedMsg = object
- info: TLineInfo
- msg: string
- sev: Severity
- CachedMsgs = seq[CachedMsg]
- var
- gPort = 6000.Port
- gAddress = ""
- gMode: Mode
- gEmitEof: bool # whether we write '!EOF!' dummy lines
- gLogging = defined(logging)
- gRefresh: bool
- requests: Channel[string]
- results: Channel[Suggest]
- proc writelnToChannel(line: string) =
- results.send(Suggest(section: ideMsg, doc: line))
- proc sugResultHook(s: Suggest) =
- results.send(s)
- proc errorHook(conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
- results.send(Suggest(section: ideChk, filePath: toFullPath(conf, info),
- line: toLinenumber(info), column: toColumn(info), doc: msg,
- forth: $sev))
- proc myLog(s: string) =
- if gLogging: log(s)
- const
- seps = {':', ';', ' ', '\t'}
- Help = "usage: sug|con|def|use|dus|chk|mod|highlight|outline|known file.nim[;dirtyfile.nim]:line:col\n" &
- "type 'quit' to quit\n" &
- "type 'debug' to toggle debug mode on/off\n" &
- "type 'terse' to toggle terse mode on/off"
- type
- EUnexpectedCommand = object of Exception
- proc parseQuoted(cmd: string; outp: var string; start: int): int =
- var i = start
- i += skipWhitespace(cmd, i)
- if cmd[i] == '"':
- i += parseUntil(cmd, outp, '"', i+1)+2
- else:
- i += parseUntil(cmd, outp, seps, i)
- result = i
- proc sexp(s: IdeCmd|TSymKind|PrefixMatch): SexpNode = sexp($s)
- proc sexp(s: Suggest): SexpNode =
- # If you change the order here, make sure to change it over in
- # nim-mode.el too.
- let qp = if s.qualifiedPath.len == 0: @[] else: s.qualifiedPath
- result = convertSexp([
- s.section,
- TSymKind s.symkind,
- qp.map(newSString),
- s.filePath,
- s.forth,
- s.line,
- s.column,
- s.doc,
- s.quality
- ])
- if s.section == ideSug:
- result.add convertSexp(s.prefix)
- proc sexp(s: seq[Suggest]): SexpNode =
- result = newSList()
- for sug in s:
- result.add(sexp(sug))
- proc listEpc(): SexpNode =
- # This function is called from Emacs to show available options.
- let
- 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"]:
- let
- cmd = sexp(command)
- methodDesc = newSList()
- methodDesc.add(cmd)
- methodDesc.add(argspecs)
- methodDesc.add(docstring)
- result.add(methodDesc)
- proc findNode(n: PNode; trackPos: TLineInfo): PSym =
- #echo "checking node ", n.info
- if n.kind == nkSym:
- if isTracked(n.info, trackPos, n.sym.name.s.len): return n.sym
- else:
- for i in 0 ..< safeLen(n):
- let res = findNode(n[i], trackPos)
- if res != nil: return res
- proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo): PSym =
- let m = graph.getModule(trackPos.fileIndex)
- if m != nil and m.ast != nil:
- result = findNode(m.ast, trackPos)
- proc execute(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int;
- graph: ModuleGraph) =
- let conf = graph.config
- myLog("cmd: " & $cmd & ", file: " & file.string &
- ", dirtyFile: " & dirtyfile.string &
- "[" & $line & ":" & $col & "]")
- conf.ideCmd = cmd
- if cmd == ideChk:
- conf.structuredErrorHook = errorHook
- conf.writelnHook = myLog
- else:
- conf.structuredErrorHook = nil
- conf.writelnHook = myLog
- if cmd == ideUse and conf.suggestVersion != 0:
- graph.resetAllModules()
- var isKnownFile = true
- let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
- if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile)
- else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"")
- conf.m.trackPos = newLineInfo(dirtyIdx, line, col)
- conf.m.trackPosAttached = false
- conf.errorCounter = 0
- if conf.suggestVersion == 1:
- graph.usageSym = nil
- if not isKnownFile:
- graph.compileProject()
- if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and
- dirtyfile.isEmpty:
- discard "no need to recompile anything"
- else:
- let modIdx = graph.parentModule(dirtyIdx)
- graph.markDirty dirtyIdx
- graph.markClientsDirty dirtyIdx
- if conf.ideCmd != ideMod:
- graph.compileProject(modIdx)
- if conf.ideCmd in {ideUse, ideDus}:
- let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym
- if u != nil:
- listUsages(conf, u)
- else:
- localError(conf, conf.m.trackPos, "found no symbol at this position " & (conf $ conf.m.trackPos))
- proc executeEpc(cmd: IdeCmd, args: SexpNode;
- graph: ModuleGraph) =
- let
- file = AbsoluteFile args[0].getStr
- line = args[1].getNum
- column = args[2].getNum
- var dirtyfile = AbsoluteFile""
- if len(args) > 3:
- dirtyfile = AbsoluteFile args[3].getStr("")
- execute(cmd, file, dirtyfile, int(line), int(column), graph)
- proc returnEpc(socket: Socket, uid: BiggestInt, s: SexpNode|string,
- return_symbol = "return") =
- let response = $convertSexp([newSSymbol(return_symbol), uid, s])
- socket.send(toHex(len(response), 6))
- socket.send(response)
- template checkSanity(client, sizeHex, size, messageBuffer: typed) =
- if client.recv(sizeHex, 6) != 6:
- raise newException(ValueError, "didn't get all the hexbytes")
- if parseHex(sizeHex, size) == 0:
- raise newException(ValueError, "invalid size hex: " & $sizeHex)
- if client.recv(messageBuffer, size) != size:
- raise newException(ValueError, "didn't get all the bytes")
- proc toStdout() {.gcsafe.} =
- while true:
- let res = results.recv()
- case res.section
- of ideNone: break
- of ideMsg: echo res.doc
- of ideKnown: echo res.quality == 1
- else: echo res
- proc toSocket(stdoutSocket: Socket) {.gcsafe.} =
- while true:
- let res = results.recv()
- case res.section
- of ideNone: break
- of ideMsg: stdoutSocket.send(res.doc & "\c\L")
- of ideKnown: stdoutSocket.send($(res.quality == 1) & "\c\L")
- else: stdoutSocket.send($res & "\c\L")
- proc toEpc(client: Socket; uid: BiggestInt) {.gcsafe.} =
- var list = newSList()
- while true:
- let res = results.recv()
- case res.section
- of ideNone: break
- of ideMsg:
- list.add sexp(res.doc)
- of ideKnown:
- list.add sexp(res.quality == 1)
- else:
- list.add sexp(res)
- returnEpc(client, uid, list)
- template setVerbosity(level: typed) =
- gVerbosity = level
- conf.notes = NotesVerbosity[gVerbosity]
- proc connectToNextFreePort(server: Socket, host: string): Port =
- server.bindaddr(Port(0), host)
- let (_, port) = server.getLocalAddr
- result = port
- type
- ThreadParams = tuple[port: Port; address: string]
- proc replStdinSingleCmd(line: string) =
- requests.send line
- toStdout()
- echo ""
- flushFile(stdout)
- proc replStdin(x: ThreadParams) {.thread.} =
- if gEmitEof:
- echo DummyEof
- while true:
- let line = readLine(stdin)
- requests.send line
- if line == "quit": break
- toStdout()
- echo DummyEof
- flushFile(stdout)
- else:
- echo Help
- var line = ""
- while readLineFromStdin("> ", line):
- replStdinSingleCmd(line)
- requests.send "quit"
- proc replCmdline(x: ThreadParams) {.thread.} =
- replStdinSingleCmd(x.address)
- requests.send "quit"
- proc replTcp(x: ThreadParams) {.thread.} =
- var server = newSocket()
- server.bindAddr(x.port, x.address)
- var inp = "".TaintedString
- server.listen()
- while true:
- var stdoutSocket = newSocket()
- accept(server, stdoutSocket)
- stdoutSocket.readLine(inp)
- requests.send inp
- toSocket(stdoutSocket)
- stdoutSocket.send("\c\L")
- stdoutSocket.close()
- proc argsToStr(x: SexpNode): string =
- if x.kind != SList: return x.getStr
- doAssert x.kind == SList
- doAssert x.len >= 4
- let file = x[0].getStr
- let line = x[1].getNum
- let col = x[2].getNum
- let dirty = x[3].getStr
- result = x[0].getStr.escape
- if dirty.len > 0:
- result.add ';'
- result.add dirty.escape
- result.add ':'
- result.add line
- result.add ':'
- result.add col
- proc replEpc(x: ThreadParams) {.thread.} =
- var server = newSocket()
- let port = connectToNextFreePort(server, "localhost")
- server.listen()
- echo port
- stdout.flushFile()
- var client = newSocket()
- # Wait for connection
- accept(server, client)
- while true:
- var
- sizeHex = ""
- size = 0
- messageBuffer = ""
- checkSanity(client, sizeHex, size, messageBuffer)
- let
- message = parseSexp($messageBuffer)
- epcApi = message[0].getSymbol
- case epcApi
- of "call":
- let
- uid = message[1].getNum
- cmd = message[2].getSymbol
- args = message[3]
- when false:
- x.ideCmd[] = parseIdeCmd(message[2].getSymbol)
- case x.ideCmd[]
- of ideSug, ideCon, ideDef, ideUse, ideDus, ideOutline, ideHighlight:
- setVerbosity(0)
- else: discard
- let fullCmd = cmd & " " & args.argsToStr
- myLog "MSG CMD: " & fullCmd
- requests.send(fullCmd)
- toEpc(client, uid)
- of "methods":
- returnEpc(client, message[1].getNum, listEpc())
- of "epc-error":
- # an unhandled exception forces down the whole process anyway, so we
- # use 'quit' here instead of 'raise'
- quit("received epc error: " & $messageBuffer)
- else:
- let errMessage = case epcApi
- of "return", "return-error":
- "no return expected"
- else:
- "unexpected call: " & epcAPI
- quit errMessage
- proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) =
- let conf = graph.config
- template sentinel() =
- # send sentinel for the input reading thread:
- results.send(Suggest(section: ideNone))
- template toggle(sw) =
- if sw in conf.globalOptions:
- excl(conf.globalOptions, sw)
- else:
- incl(conf.globalOptions, sw)
- sentinel()
- return
- template err() =
- echo Help
- sentinel()
- return
- var opc = ""
- var i = parseIdent(cmd, opc, 0)
- case opc.normalize
- of "sug": conf.ideCmd = ideSug
- of "con": conf.ideCmd = ideCon
- of "def": conf.ideCmd = ideDef
- of "use": conf.ideCmd = ideUse
- of "dus": conf.ideCmd = ideDus
- of "mod": conf.ideCmd = ideMod
- of "chk": conf.ideCmd = ideChk
- of "highlight": conf.ideCmd = ideHighlight
- of "outline": conf.ideCmd = ideOutline
- of "quit":
- sentinel()
- quit()
- of "debug": toggle optIdeDebug
- of "terse": toggle optIdeTerse
- of "known": conf.ideCmd = ideKnown
- else: err()
- var dirtyfile = ""
- var orig = ""
- i = parseQuoted(cmd, orig, i)
- if cmd[i] == ';':
- i = parseQuoted(cmd, dirtyfile, i+1)
- i += skipWhile(cmd, seps, i)
- var line = -1
- var col = 0
- i += parseInt(cmd, line, i)
- i += skipWhile(cmd, seps, i)
- i += parseInt(cmd, col, i)
- if conf.ideCmd == ideKnown:
- results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, AbsoluteFile orig))))
- 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)
- sentinel()
- proc recompileFullProject(graph: ModuleGraph) =
- #echo "recompiling full project"
- resetSystemArtifacts(graph)
- graph.vm = nil
- graph.resetAllModules()
- GC_fullcollect()
- compileProject(graph)
- #echo GC_getStatistics()
- proc mainThread(graph: ModuleGraph) =
- let conf = graph.config
- if gLogging:
- for it in conf.searchPaths:
- log(it.string)
- proc wrHook(line: string) {.closure.} =
- if gMode == mepc:
- if gLogging: log(line)
- else:
- writelnToChannel(line)
- conf.writelnHook = wrHook
- conf.suggestionResultHook = sugResultHook
- graph.doStopCompile = proc (): bool = requests.peek() > 0
- var idle = 0
- var cachedMsgs: CachedMsgs = @[]
- while true:
- let (hasData, req) = requests.tryRecv()
- if hasData:
- conf.writelnHook = wrHook
- conf.suggestionResultHook = sugResultHook
- execCmd(req, graph, cachedMsgs)
- idle = 0
- else:
- os.sleep 250
- idle += 1
- if idle == 20 and gRefresh:
- # we use some nimsuggest activity to enable a lazy recompile:
- conf.ideCmd = ideChk
- conf.writelnHook = proc (s: string) = discard
- cachedMsgs.setLen 0
- conf.structuredErrorHook = proc (conf: ConfigRef; info: TLineInfo; msg: string; sev: Severity) =
- cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev))
- conf.suggestionResultHook = proc (s: Suggest) = discard
- recompileFullProject(graph)
- var
- inputThread: Thread[ThreadParams]
- proc mainCommand(graph: ModuleGraph) =
- let conf = graph.config
- clearPasses(graph)
- registerPass graph, verbosePass
- registerPass graph, semPass
- conf.cmd = cmdIdeTools
- wantMainModule(conf)
- if not fileExists(conf.projectFull):
- quit "cannot find file: " & conf.projectFull.string
- add(conf.searchPaths, conf.libpath)
- # do not stop after the first error:
- conf.errorMax = high(int)
- # do not print errors, but log them
- conf.writelnHook = proc (s: string) = log(s)
- conf.structuredErrorHook = nil
- # compile the project before showing any input so that we already
- # can answer questions right away:
- compileProject(graph)
- open(requests)
- open(results)
- case gMode
- of mstdin: createThread(inputThread, replStdin, (gPort, gAddress))
- of mtcp: createThread(inputThread, replTcp, (gPort, gAddress))
- of mepc: createThread(inputThread, replEpc, (gPort, gAddress))
- of mcmdsug: createThread(inputThread, replCmdline,
- (gPort, "sug \"" & conf.projectFull.string & "\":" & gAddress))
- of mcmdcon: createThread(inputThread, replCmdline,
- (gPort, "con \"" & conf.projectFull.string & "\":" & gAddress))
- mainThread(graph)
- joinThread(inputThread)
- close(requests)
- close(results)
- proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
- var p = parseopt.initOptParser(cmd)
- while true:
- parseopt.next(p)
- case p.kind
- of cmdEnd: break
- of cmdLongoption, cmdShortOption:
- case p.key.normalize
- of "help", "h":
- stdout.writeline(Usage)
- quit()
- of "port":
- gPort = parseInt(p.val).Port
- gMode = mtcp
- of "address":
- gAddress = p.val
- gMode = mtcp
- of "stdin": gMode = mstdin
- of "cmdsug":
- gMode = mcmdsug
- gAddress = p.val
- incl(conf.globalOptions, optIdeDebug)
- of "cmdcon":
- gMode = mcmdcon
- gAddress = p.val
- incl(conf.globalOptions, optIdeDebug)
- of "epc":
- gMode = mepc
- conf.verbosity = 0 # Port number gotta be first.
- of "debug": incl(conf.globalOptions, optIdeDebug)
- of "v2": conf.suggestVersion = 0
- of "v1": conf.suggestVersion = 1
- of "tester":
- gMode = mstdin
- gEmitEof = true
- gRefresh = false
- of "log": gLogging = true
- of "refresh":
- if p.val.len > 0:
- gRefresh = parseBool(p.val)
- else:
- gRefresh = true
- of "maxresults":
- conf.suggestMaxResults = parseInt(p.val)
- else: processSwitch(pass, p, conf)
- of cmdArgument:
- let a = unixToNativePath(p.key)
- if dirExists(a) and not fileExists(a.addFileExt("nim")):
- conf.projectName = findProjectNimFile(conf, a)
- # don't make it worse, report the error the old way:
- if conf.projectName.len == 0: conf.projectName = a
- else:
- conf.projectName = a
- # if processArgument(pass, p, argsCount): break
- proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
- let self = NimProg(
- suggestMode: true,
- processCmdLine: processCmdLine,
- mainCommand: mainCommand
- )
- self.initDefinesProg(conf, "nimsuggest")
- if paramCount() == 0:
- stdout.writeline(Usage)
- return
- self.processCmdLineAndProjectPath(conf)
- 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""
- #msgs.writelnHook = proc (line: string) = log(line)
- myLog("START " & conf.projectFull.string)
- discard self.loadConfigsAndRunMainCommand(cache, conf)
- handleCmdline(newIdentCache(), newConfigRef())
|