#
#
# 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(dirtyIdx)
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:
if isKnownFile:
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)
when isMainModule:
handleCmdline(newIdentCache(), newConfigRef())
else:
export Suggest
export IdeCmd
export AbsoluteFile
type NimSuggest* = ref object
graph: ModuleGraph
idle: int
cachedMsgs: CachedMsgs
proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
var retval: ModuleGraph
proc mockCommand(graph: ModuleGraph) =
retval = graph
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)
proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
conf.suggestVersion = 0
let a = unixToNativePath(project)
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
let
cache = newIdentCache()
conf = newConfigRef()
self = NimProg(
suggestMode: true,
processCmdLine: mockCmdLine,
mainCommand: mockCommand
)
self.initDefinesProg(conf, "nimsuggest")
self.processCmdLineAndProjectPath(conf)
if gMode != mstdin:
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""
else:
conf.prefixDir = AbsoluteDir nimPath
#msgs.writelnHook = proc (line: string) = log(line)
myLog("START " & conf.projectFull.string)
discard self.loadConfigsAndRunMainCommand(cache, conf)
if gLogging:
for it in conf.searchPaths:
log(it.string)
retval.doStopCompile = proc (): bool = false
return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
var retval: seq[Suggest] = @[]
let conf = nimsuggest.graph.config
conf.ideCmd = cmd
conf.writelnHook = proc (line: string) =
retval.add(Suggest(section: ideMsg, doc: line))
conf.suggestionResultHook = proc (s: Suggest) =
retval.add(s)
if conf.ideCmd == ideKnown:
retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
else:
if conf.ideCmd == ideChk:
for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
execute(conf.ideCmd, file, dirtyfile, line, col, nimsuggest.graph)
return retval