summary refs log tree commit diff stats
path: root/tools/nimsuggest/nimsuggest.nim
diff options
context:
space:
mode:
authorAraq <rumpf_a@web.de>2016-10-31 20:12:54 +0100
committerAraq <rumpf_a@web.de>2016-10-31 20:12:54 +0100
commit08c94ef6b5beb3afb43787b406c61b0795ce66e2 (patch)
tree66a665e6c4d427e9bd771862fcd26bf40d470460 /tools/nimsuggest/nimsuggest.nim
parent29db0d8585a0b2d3297a1e7745bfb92bf0c943b7 (diff)
downloadNim-08c94ef6b5beb3afb43787b406c61b0795ce66e2.tar.gz
nimsuggest is now part of Nim
Diffstat (limited to 'tools/nimsuggest/nimsuggest.nim')
-rw-r--r--tools/nimsuggest/nimsuggest.nim477
1 files changed, 477 insertions, 0 deletions
diff --git a/tools/nimsuggest/nimsuggest.nim b/tools/nimsuggest/nimsuggest.nim
new file mode 100644
index 000000000..c6a6bce05
--- /dev/null
+++ b/tools/nimsuggest/nimsuggest.nim
@@ -0,0 +1,477 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2016 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.
+
+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, compiler/commands, compiler/modules, compiler/sem,
+  compiler/passes, compiler/passaux, compiler/msgs, compiler/nimconf,
+  compiler/extccomp, compiler/condsyms, compiler/lists,
+  compiler/sigmatch, compiler/ast, compiler/scriptconfig,
+  compiler/idents
+
+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
+  --v2                    use version 2 of the protocol; more features and
+                          much faster
+  --tester                implies --v2 and --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
+
+var
+  gPort = 6000.Port
+  gAddress = ""
+  gMode: Mode
+  gEmitEof: bool # whether we write '!EOF!' dummy lines
+  gLogging = false
+
+const
+  seps = {':', ';', ' ', '\t'}
+  Help = "usage: sug|con|def|use|dus|chk|highlight|outline 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 logStr(line: string) =
+  var f: File
+  if open(f, getHomeDir() / "nimsuggest.log", fmAppend):
+    f.writeLine(line)
+    f.close()
+
+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): 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.
+  result = convertSexp([
+    s.section,
+    s.symkind,
+    s.qualifiedPath.map(newSString),
+    s.filePath,
+    s.forth,
+    s.line,
+    s.column,
+    s.doc
+  ])
+
+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"]:
+    let
+      cmd = sexp(command)
+      methodDesc = newSList()
+    methodDesc.add(cmd)
+    methodDesc.add(argspecs)
+    methodDesc.add(docstring)
+    result.add(methodDesc)
+
+proc findNode(n: PNode): PSym =
+  #echo "checking node ", n.info
+  if n.kind == nkSym:
+    if isTracked(n.info, n.sym.name.s.len): return n.sym
+  else:
+    for i in 0 ..< safeLen(n):
+      let res = n.sons[i].findNode
+      if res != nil: return res
+
+proc symFromInfo(gTrackPos: TLineInfo): PSym =
+  let m = getModule(gTrackPos.fileIndex)
+  #echo m.isNil, " I knew it ", gTrackPos.fileIndex
+  if m != nil and m.ast != nil:
+    result = m.ast.findNode
+
+proc execute(cmd: IdeCmd, file, dirtyfile: string, line, col: int;
+             cache: IdentCache) =
+  if gLogging:
+    logStr("cmd: " & $cmd & ", file: " & file & ", dirtyFile: " & dirtyfile & "[" & $line & ":" & $col & "]")
+  gIdeCmd = cmd
+  if cmd == ideUse and suggestVersion != 2:
+    modules.resetAllModules()
+  var isKnownFile = true
+  let dirtyIdx = file.fileInfoIdx(isKnownFile)
+
+  if dirtyfile.len != 0: msgs.setDirtyFile(dirtyIdx, dirtyfile)
+  else: msgs.setDirtyFile(dirtyIdx, nil)
+
+  gTrackPos = newLineInfo(dirtyIdx, line, col)
+  gErrorCounter = 0
+  if suggestVersion < 2:
+    usageSym = nil
+  if not isKnownFile:
+    compileProject(cache)
+  if suggestVersion == 2 and gIdeCmd in {ideUse, ideDus} and
+      dirtyfile.len == 0:
+    discard "no need to recompile anything"
+  else:
+    resetModule dirtyIdx
+    if dirtyIdx != gProjectMainIdx:
+      resetModule gProjectMainIdx
+    compileProject(cache, dirtyIdx)
+  if gIdeCmd in {ideUse, ideDus}:
+    let u = if suggestVersion >= 2: symFromInfo(gTrackPos) else: usageSym
+    if u != nil:
+      listUsages(u)
+    else:
+      localError(gTrackPos, "found no symbol at this position " & $gTrackPos)
+
+proc executeEpc(cmd: IdeCmd, args: SexpNode; cache: IdentCache) =
+  let
+    file = args[0].getStr
+    line = args[1].getNum
+    column = args[2].getNum
+  var dirtyfile = ""
+  if len(args) > 3:
+    dirtyfile = args[3].getStr(nil)
+  execute(cmd, file, dirtyfile, int(line), int(column), cache)
+
+proc returnEpc(socket: var 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 sendEpc(results: typed, tdef, hook: untyped) =
+  hook = proc (s: tdef) =
+    results.add(
+      # Put newlines to parse output by flycheck-nim.el
+      when results is string: s & "\n"
+      else: s
+    )
+
+  executeEpc(gIdeCmd, args, cache)
+  let res = sexp(results)
+  if gLogging:
+    logStr($res)
+  returnEPC(client, uid, res)
+
+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")
+
+template setVerbosity(level: typed) =
+  gVerbosity = level
+  gNotes = NotesVerbosity[gVerbosity]
+
+proc connectToNextFreePort(server: Socket, host: string): Port =
+  server.bindaddr(Port(0), host)
+  let (_, port) = server.getLocalAddr
+  result = port
+
+proc parseCmdLine(cmd: string; cache: IdentCache) =
+  template toggle(sw) =
+    if sw in gGlobalOptions:
+      excl(gGlobalOptions, sw)
+    else:
+      incl(gGlobalOptions, sw)
+    return
+
+  template err() =
+    echo Help
+    return
+
+  var opc = ""
+  var i = parseIdent(cmd, opc, 0)
+  case opc.normalize
+  of "sug": gIdeCmd = ideSug
+  of "con": gIdeCmd = ideCon
+  of "def": gIdeCmd = ideDef
+  of "use": gIdeCmd = ideUse
+  of "dus": gIdeCmd = ideDus
+  of "chk":
+    gIdeCmd = ideChk
+    incl(gGlobalOptions, optIdeDebug)
+  of "highlight": gIdeCmd = ideHighlight
+  of "outline": gIdeCmd = ideOutline
+  of "quit": quit()
+  of "debug": toggle optIdeDebug
+  of "terse": toggle optIdeTerse
+  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)
+
+  execute(gIdeCmd, orig, dirtyfile, line, col-1, cache)
+
+proc serveStdin(cache: IdentCache) =
+  if gEmitEof:
+    echo DummyEof
+    while true:
+      let line = readLine(stdin)
+      parseCmdLine line, cache
+      echo DummyEof
+      flushFile(stdout)
+  else:
+    echo Help
+    var line = ""
+    while readLineFromStdin("> ", line):
+      parseCmdLine line, cache
+      echo ""
+      flushFile(stdout)
+
+proc serveTcp(cache: IdentCache) =
+  var server = newSocket()
+  server.bindAddr(gPort, gAddress)
+  var inp = "".TaintedString
+  server.listen()
+
+  while true:
+    var stdoutSocket = newSocket()
+    msgs.writelnHook = proc (line: string) =
+      stdoutSocket.send(line & "\c\L")
+
+    accept(server, stdoutSocket)
+
+    stdoutSocket.readLine(inp)
+    parseCmdLine inp.string, cache
+
+    stdoutSocket.send("\c\L")
+    stdoutSocket.close()
+
+proc serveEpc(server: Socket; cache: IdentCache) =
+  var client = newSocket()
+  # Wait for connection
+  accept(server, client)
+  if gLogging:
+    var it = searchPaths.head
+    while it != nil:
+      logStr(PStrEntry(it).data)
+      it = it.next
+    msgs.writelnHook = proc (line: string) = logStr(line)
+
+  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
+        args = message[3]
+
+      gIdeCmd = parseIdeCmd(message[2].getSymbol)
+      case gIdeCmd
+      of ideChk:
+        setVerbosity(1)
+        # Use full path because other emacs plugins depends it
+        gListFullPaths = true
+        incl(gGlobalOptions, optIdeDebug)
+        var hints_or_errors = ""
+        sendEpc(hints_or_errors, string, msgs.writelnHook)
+      of ideSug, ideCon, ideDef, ideUse, ideDus, ideOutline, ideHighlight:
+        setVerbosity(0)
+        var suggests: seq[Suggest] = @[]
+        sendEpc(suggests, Suggest, suggestionResultHook)
+      else: discard
+    of "methods":
+      returnEpc(client, message[1].getNum, listEPC())
+    of "epc-error":
+      stderr.writeline("recieved epc error: " & $messageBuffer)
+      raise newException(IOError, "epc error")
+    else:
+      let errMessage = case epcAPI
+                       of "return", "return-error":
+                         "no return expected"
+                       else:
+                         "unexpected call: " & epcAPI
+      raise newException(EUnexpectedCommand, errMessage)
+
+template beCompatible() =
+  when compiles(modules.gFuzzyGraphChecking):
+    modules.gFuzzyGraphChecking = true
+
+proc mainCommand(cache: IdentCache) =
+  clearPasses()
+  registerPass verbosePass
+  registerPass semPass
+  gCmd = cmdIdeTools
+  incl gGlobalOptions, optCaasEnabled
+  isServing = true
+  wantMainModule()
+  appendStr(searchPaths, options.libpath)
+  #if gProjectFull.len != 0:
+    # current path is always looked first for modules
+  #  prependStr(searchPaths, gProjectPath)
+
+  # do not stop after the first error:
+  msgs.gErrorMax = high(int)
+
+  case gMode
+  of mstdin:
+    beCompatible()
+    compileProject(cache)
+    #modules.gFuzzyGraphChecking = false
+    serveStdin(cache)
+  of mtcp:
+    # until somebody accepted the connection, produce no output (logging is too
+    # slow for big projects):
+    msgs.writelnHook = proc (msg: string) = discard
+    beCompatible()
+    compileProject(cache)
+    #modules.gFuzzyGraphChecking = false
+    serveTcp(cache)
+  of mepc:
+    beCompatible()
+    var server = newSocket()
+    let port = connectToNextFreePort(server, "localhost")
+    server.listen()
+    echo port
+    compileProject(cache)
+    serveEpc(server, cache)
+
+proc processCmdLine*(pass: TCmdLinePass, cmd: string) =
+  var p = parseopt.initOptParser(cmd)
+  while true:
+    parseopt.next(p)
+    case p.kind
+    of cmdEnd: break
+    of cmdLongoption, cmdShortOption:
+      case p.key.normalize
+      of "port":
+        gPort = parseInt(p.val).Port
+        gMode = mtcp
+      of "address":
+        gAddress = p.val
+        gMode = mtcp
+      of "stdin": gMode = mstdin
+      of "epc":
+        gMode = mepc
+        gVerbosity = 0          # Port number gotta be first.
+      of "debug":
+        incl(gGlobalOptions, optIdeDebug)
+      of "v2":
+        suggestVersion = 2
+      of "tester":
+        suggestVersion = 2
+        gMode = mstdin
+        gEmitEof = true
+      of "log":
+        gLogging = true
+      else: processSwitch(pass, p)
+    of cmdArgument:
+      options.gProjectName = unixToNativePath(p.key)
+      # if processArgument(pass, p, argsCount): break
+
+proc handleCmdLine(cache: IdentCache) =
+  if paramCount() == 0:
+    stdout.writeline(Usage)
+  else:
+    processCmdLine(passCmd1, "")
+    if gMode != mstdin:
+      msgs.writelnHook = proc (msg: string) = discard
+    if gProjectName != "":
+      try:
+        gProjectFull = canonicalizePath(gProjectName)
+      except OSError:
+        gProjectFull = gProjectName
+      var p = splitFile(gProjectFull)
+      gProjectPath = p.dir
+      gProjectName = p.name
+    else:
+      gProjectPath = getCurrentDir()
+
+    # Find Nim's prefix dir.
+    let binaryPath = findExe("nim")
+    if binaryPath == "":
+      raise newException(IOError,
+          "Cannot find Nim standard library: Nim compiler not in PATH")
+    gPrefixDir = binaryPath.splitPath().head.parentDir()
+    #msgs.writelnHook = proc (line: string) = logStr(line)
+
+    loadConfigs(DefaultConfig, cache) # load all config files
+    # now process command line arguments again, because some options in the
+    # command line can overwite the config file's settings
+    options.command = "nimsuggest"
+    let scriptFile = gProjectFull.changeFileExt("nims")
+    if fileExists(scriptFile):
+      runNimScript(cache, scriptFile, freshDefines=false)
+      # 'nim foo.nims' means to just run the NimScript file and do nothing more:
+      if scriptFile == gProjectFull: return
+    elif fileExists(gProjectPath / "config.nims"):
+      # directory wide NimScript file
+      runNimScript(cache, gProjectPath / "config.nims", freshDefines=false)
+
+    extccomp.initVars()
+    processCmdLine(passCmd2, "")
+
+    mainCommand(cache)
+
+when false:
+  proc quitCalled() {.noconv.} =
+    writeStackTrace()
+
+  addQuitProc(quitCalled)
+
+condsyms.initDefines()
+defineSymbol "nimsuggest"
+handleCmdline(newIdentCache())