summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/prefixmatches.nim86
-rw-r--r--compiler/suggest.nim190
-rw-r--r--tools/nimsuggest/nimsuggest.nim36
-rw-r--r--tools/nimsuggest/tester.nim15
-rw-r--r--tools/nimsuggest/tests/tdot1.nim6
-rw-r--r--tools/nimsuggest/tests/tdot2.nim14
-rw-r--r--tools/nimsuggest/tests/tdot3.nim14
-rw-r--r--tools/nimsuggest/tests/tno_deref.nim2
-rw-r--r--tools/nimsuggest/tests/tsug_regression.nim28
-rw-r--r--tools/nimsuggest/tests/twithin_macro.nim12
-rw-r--r--tools/nimsuggest/tests/twithin_macro_prefix.nim4
11 files changed, 309 insertions, 98 deletions
diff --git a/compiler/prefixmatches.nim b/compiler/prefixmatches.nim
new file mode 100644
index 000000000..9805fdbd2
--- /dev/null
+++ b/compiler/prefixmatches.nim
@@ -0,0 +1,86 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2017 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+from strutils import toLowerAscii
+
+type
+  PrefixMatch* {.pure.} = enum
+    None,   ## no prefix detected
+    Abbrev  ## prefix is an abbreviation of the symbol
+    Substr, ## prefix is a substring of the symbol
+    Prefix, ## prefix does match the symbol
+
+proc prefixMatch*(p, s: string): PrefixMatch =
+  template eq(a, b): bool = a.toLowerAscii == b.toLowerAscii
+  if p.len > s.len: return PrefixMatch.None
+  var i = 0
+  let L = s.len
+  # check for prefix/contains:
+  while i < L:
+    if s[i] == '_': inc i
+    if eq(s[i], p[0]):
+      var ii = i+1
+      var jj = 1
+      while ii < L and jj < p.len:
+        if p[jj] == '_': inc jj
+        if s[ii] == '_': inc ii
+        if not eq(s[ii], p[jj]): break
+        inc ii
+        inc jj
+
+      if jj >= p.len:
+        if i == 0: return PrefixMatch.Prefix
+        else: return PrefixMatch.Substr
+    inc i
+  # check for abbrev:
+  if eq(s[0], p[0]):
+    i = 1
+    var j = 1
+    while i < s.len:
+      if s[i] == '_' and i < s.len-1:
+        if j < p.len and eq(p[j], s[i+1]): inc j
+        else: return PrefixMatch.None
+      if s[i] in {'A'..'Z'} and s[i-1] notin {'A'..'Z'}:
+        if j < p.len and eq(p[j], s[i]): inc j
+        else: return PrefixMatch.None
+      inc i
+    return PrefixMatch.Abbrev
+  return PrefixMatch.None
+
+when isMainModule:
+  import macros
+
+  macro check(val, body: untyped): untyped =
+    result = newStmtList()
+    expectKind body, nnkStmtList
+    for b in body:
+      expectKind b, nnkPar
+      expectLen b, 2
+      let p = b[0]
+      let s = b[1]
+      result.add quote do:
+        echo prefixMatch(`p`, `s`) == `val`
+
+  check PrefixMatch.Prefix:
+    ("abc", "abc")
+    ("a", "abc")
+    ("xyz", "X_yzzzZe")
+
+  check PrefixMatch.Substr:
+    ("b", "abc")
+    ("abc", "fooabcabc")
+    ("abC", "foo_AB_c")
+
+  check PrefixMatch.Abbrev:
+    ("abc", "AxxxBxxxCxxx")
+    ("xyz", "X_yabcZe")
+
+  check PrefixMatch.None:
+    ("foobar", "afkslfjd_as")
+    ("xyz", "X_yuuZuuZe")
diff --git a/compiler/suggest.nim b/compiler/suggest.nim
index 6aaca4185..ebabed465 100644
--- a/compiler/suggest.nim
+++ b/compiler/suggest.nim
@@ -32,7 +32,7 @@
 
 # included from sigmatch.nim
 
-import algorithm, sequtils
+import algorithm, prefixmatches
 
 when defined(nimsuggest):
   import passes, tables # importer
@@ -41,9 +41,11 @@ const
   sep = '\t'
 
 type
-  Suggest* = object
+  Suggest* = ref object
     section*: IdeCmd
     qualifiedPath*: seq[string]
+    name*: PIdent                # not used beyond sorting purposes; name is also
+                                 # part of 'qualifiedPath'
     filePath*: string
     line*: int                   # Starts at 1
     column*: int                 # Starts at 0
@@ -52,7 +54,11 @@ type
     forth*: string               # type
     quality*: range[0..100]   # matching quality
     isGlobal*: bool # is a global variable
+    contextFits*: bool # type/non-type context matches
+    prefix*: PrefixMatch
+    scope*, localUsages*, globalUsages*: int # more usages is better
     tokenLen*: int
+  Suggestions* = seq[Suggest]
 
 var
   suggestionResultHook*: proc (result: Suggest) {.closure.}
@@ -65,23 +71,57 @@ template origModuleName(m: PSym): string = m.name.s
 proc findDocComment(n: PNode): PNode =
   if n == nil: return nil
   if not isNil(n.comment): return n
-  for i in countup(0, safeLen(n)-1):
-    result = findDocComment(n.sons[i])
+  if n.kind in {nkStmtList, nkStmtListExpr} and n.len > 0:
+    result = findDocComment(n.sons[0])
     if result != nil: return
+    if n.len > 1:
+      result = findDocComment(n.sons[1])
+  elif n.kind in {nkAsgn, nkFastAsgn} and n.len == 2:
+    result = findDocComment(n.sons[1])
 
 proc extractDocComment(s: PSym): string =
-  let n = findDocComment(s.ast)
+  var n = findDocComment(s.ast)
+  if n.isNil and s.kind in routineKinds and s.ast != nil:
+    n = findDocComment(s.ast[bodyPos])
   if not n.isNil:
     result = n.comment.replace("\n##", "\n").strip
   else:
     result = ""
 
+proc cmpSuggestions(a, b: Suggest): int =
+  template cf(field) {.dirty.} =
+    result = b.field.int - a.field.int
+    if result != 0: return result
+
+  cf scope
+  cf prefix
+  # when the first type matches, it's better when it's a generic match:
+  cf quality
+  cf contextFits
+  cf localUsages
+  cf globalUsages
+  # if all is equal, sort alphabetically for deterministic output,
+  # independent of hashing order:
+  result = cmp(a.name.s, b.name.s)
+
 proc symToSuggest(s: PSym, isLocal: bool, section: string, li: TLineInfo;
-                  quality: range[0..100]): Suggest =
+                  quality: range[0..100]; prefix: PrefixMatch;
+                  inTypeContext: bool; scope: int): Suggest =
+  new(result)
   result.section = parseIdeCmd(section)
   result.quality = quality
   result.isGlobal = sfGlobal in s.flags
   result.tokenLen = s.name.s.len
+  result.prefix = prefix
+  result.contextFits = inTypeContext == (s.kind in {skType, skGenericParam})
+  result.scope = scope
+  result.name = s.name
+  when defined(nimsuggest):
+    result.globalUsages = s.allUsages.len
+    var c = 0
+    for u in s.allUsages:
+      if u.fileIndex == li.fileIndex: inc c
+    result.localUsages = c
   if optIdeTerse in gGlobalOptions:
     result.symkind = s.kind
     result.filePath = toFullPath(li)
@@ -144,10 +184,14 @@ proc `$`*(suggest: Suggest): string =
     if suggestVersion == 2:
       result.add(sep)
       result.add($suggest.quality)
+      if suggest.section == ideSug:
+        result.add(sep)
+        result.add($suggest.prefix)
 
 proc symToSuggest(s: PSym, isLocal: bool, section: string;
-                  quality: range[0..100]): Suggest =
-  result = symToSuggest(s, isLocal, section, s.info, quality)
+                  quality: range[0..100], prefix: PrefixMatch; inTypeContext: bool;
+                  scope: int): Suggest =
+  result = symToSuggest(s, isLocal, section, s.info, quality, prefix, inTypeContext, scope)
 
 proc suggestResult(s: Suggest) =
   if not isNil(suggestionResultHook):
@@ -155,20 +199,34 @@ proc suggestResult(s: Suggest) =
   else:
     suggestWriteln($s)
 
-proc filterSym(s: PSym; prefix: PNode): bool {.inline.} =
-  proc prefixMatch(s: PSym; n: PNode): bool =
+proc produceOutput(a: var Suggestions) =
+  if gIdeCmd in {ideSug, ideCon}:
+    a.sort cmpSuggestions
+  if not isNil(suggestionResultHook):
+    for s in a:
+      suggestionResultHook(s)
+  else:
+    for s in a:
+      suggestWriteln($s)
+
+proc filterSym(s: PSym; prefix: PNode; res: var PrefixMatch): bool {.inline.} =
+  proc prefixMatch(s: PSym; n: PNode): PrefixMatch =
     case n.kind
-    of nkIdent: result = s.name.s.startsWith(n.ident.s)
-    of nkSym: result = s.name.s.startsWith(n.sym.name.s)
+    of nkIdent: result = n.ident.s.prefixMatch(s.name.s)
+    of nkSym: result = n.sym.name.s.prefixMatch(s.name.s)
     of nkOpenSymChoice, nkClosedSymChoice, nkAccQuoted:
       if n.len > 0:
         result = prefixMatch(s, n[0])
     else: discard
   if s.kind != skModule:
-    result = prefix.isNil or prefixMatch(s, prefix)
+    if prefix != nil:
+      res = prefixMatch(s, prefix)
+      result = res != PrefixMatch.None
+    else:
+      result = true
 
-proc filterSymNoOpr(s: PSym; prefix: PNode): bool {.inline.} =
-  result = filterSym(s, prefix) and s.name.s[0] in lexer.SymChars and
+proc filterSymNoOpr(s: PSym; prefix: PNode; res: var PrefixMatch): bool {.inline.} =
+  result = filterSym(s, prefix, res) and s.name.s[0] in lexer.SymChars and
      not isKeyword(s.name)
 
 proc fieldVisible*(c: PContext, f: PSym): bool {.inline.} =
@@ -179,31 +237,38 @@ proc fieldVisible*(c: PContext, f: PSym): bool {.inline.} =
       result = true
       break
 
-proc suggestField(c: PContext, s: PSym; f: PNode; outputs: var int) =
-  if filterSym(s, f) and fieldVisible(c, s):
-    suggestResult(symToSuggest(s, isLocal=true, $ideSug, 100))
-    inc outputs
+proc suggestField(c: PContext, s: PSym; f: PNode; outputs: var Suggestions) =
+  var pm: PrefixMatch
+  if filterSym(s, f, pm) and fieldVisible(c, s):
+    outputs.add(symToSuggest(s, isLocal=true, $ideSug, 100, pm, c.inTypeContext > 0, 0))
+
+proc getQuality(s: PSym): range[0..100] =
+  if s.typ != nil and s.typ.len > 1:
+    var exp = s.typ.sons[1].skipTypes({tyGenericInst, tyVar, tyAlias})
+    if exp.kind == tyVarargs: exp = elemType(exp)
+    if exp.kind in {tyExpr, tyStmt, tyGenericParam, tyAnything}: return 50
+  return 100
 
 template wholeSymTab(cond, section: untyped) =
   var isLocal = true
+  var scopeN = 0
   for scope in walkScopes(c.currentScope):
     if scope == c.topLevelScope: isLocal = false
-    var entries = sequtils.toSeq(items(scope.symbols))
-    sort(entries) do (a,b: PSym) -> int:
-      return cmp(a.name.s, b.name.s)
-    for item in entries:
+    dec scopeN
+    for item in scope.symbols:
       let it {.inject.} = item
+      var pm {.inject.}: PrefixMatch
       if cond:
-        suggestResult(symToSuggest(it, isLocal = isLocal, section, 100))
-        inc outputs
+        outputs.add(symToSuggest(it, isLocal = isLocal, section, getQuality(it),
+                                 pm, c.inTypeContext > 0, scopeN))
 
-proc suggestSymList(c: PContext, list, f: PNode, outputs: var int) =
+proc suggestSymList(c: PContext, list, f: PNode, outputs: var Suggestions) =
   for i in countup(0, sonsLen(list) - 1):
     if list.sons[i].kind == nkSym:
       suggestField(c, list.sons[i].sym, f, outputs)
     #else: InternalError(list.info, "getSymFromList")
 
-proc suggestObject(c: PContext, n, f: PNode, outputs: var int) =
+proc suggestObject(c: PContext, n, f: PNode, outputs: var Suggestions) =
   case n.kind
   of nkRecList:
     for i in countup(0, sonsLen(n)-1): suggestObject(c, n.sons[i], f, outputs)
@@ -235,8 +300,8 @@ proc argsFit(c: PContext, candidate: PSym, n, nOrig: PNode): bool =
   else:
     result = false
 
-proc suggestCall(c: PContext, n, nOrig: PNode, outputs: var int) =
-  wholeSymTab(filterSym(it, nil) and nameFits(c, it, n) and argsFit(c, it, n, nOrig),
+proc suggestCall(c: PContext, n, nOrig: PNode, outputs: var Suggestions) =
+  wholeSymTab(filterSym(it, nil, pm) and nameFits(c, it, n) and argsFit(c, it, n, nOrig),
               $ideCon)
 
 proc typeFits(c: PContext, s: PSym, firstArg: PType): bool {.inline.} =
@@ -252,25 +317,28 @@ proc typeFits(c: PContext, s: PSym, firstArg: PType): bool {.inline.} =
       if exp.kind in {tyExpr, tyStmt, tyGenericParam, tyAnything}: return
     result = sigmatch.argtypeMatches(c, s.typ.sons[1], firstArg)
 
-proc suggestOperations(c: PContext, n, f: PNode, typ: PType, outputs: var int) =
+proc suggestOperations(c: PContext, n, f: PNode, typ: PType, outputs: var Suggestions) =
   assert typ != nil
-  wholeSymTab(filterSymNoOpr(it, f) and typeFits(c, it, typ), $ideSug)
+  wholeSymTab(filterSymNoOpr(it, f, pm) and typeFits(c, it, typ), $ideSug)
 
-proc suggestEverything(c: PContext, n, f: PNode, outputs: var int) =
+proc suggestEverything(c: PContext, n, f: PNode, outputs: var Suggestions) =
   # do not produce too many symbols:
   var isLocal = true
+  var scopeN = 0
   for scope in walkScopes(c.currentScope):
     if scope == c.topLevelScope: isLocal = false
+    dec scopeN
     for it in items(scope.symbols):
-      if filterSym(it, f):
-        suggestResult(symToSuggest(it, isLocal = isLocal, $ideSug, 0))
-        inc outputs
+      var pm: PrefixMatch
+      if filterSym(it, f, pm):
+        outputs.add(symToSuggest(it, isLocal = isLocal, $ideSug, 0, pm, c.inTypeContext > 0, scopeN))
     if scope == c.topLevelScope and f.isNil: break
 
-proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var int) =
+proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var Suggestions) =
   # special code that deals with ``myObj.``. `n` is NOT the nkDotExpr-node, but
   # ``myObj``.
   var typ = n.typ
+  var pm: PrefixMatch
   when defined(nimsuggest):
     if n.kind == nkSym and n.sym.kind == skError and suggestVersion == 2:
       # consider 'foo.|' where 'foo' is some not imported module.
@@ -283,10 +351,10 @@ proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var int) =
         if m == nil: typ = nil
         else:
           for it in items(n.sym.tab):
-            if filterSym(it, field):
-              suggestResult(symToSuggest(it, isLocal=false, $ideSug, 100))
-              inc outputs
-          suggestResult(symToSuggest(m, isLocal=false, $ideMod, 100))
+            if filterSym(it, field, pm):
+              outputs.add(symToSuggest(it, isLocal=false, $ideSug, 100, pm, c.inTypeContext > 0, -100))
+          outputs.add(symToSuggest(m, isLocal=false, $ideMod, 100, PrefixMatch.None,
+            c.inTypeContext > 0, -99))
 
   if typ == nil:
     # a module symbol has no type for example:
@@ -294,14 +362,12 @@ proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var int) =
       if n.sym == c.module:
         # all symbols accessible, because we are in the current module:
         for it in items(c.topLevelScope.symbols):
-          if filterSym(it, field):
-            suggestResult(symToSuggest(it, isLocal=false, $ideSug, 100))
-            inc outputs
+          if filterSym(it, field, pm):
+            outputs.add(symToSuggest(it, isLocal=false, $ideSug, 100, pm, c.inTypeContext > 0, -99))
       else:
         for it in items(n.sym.tab):
-          if filterSym(it, field):
-            suggestResult(symToSuggest(it, isLocal=false, $ideSug, 100))
-            inc outputs
+          if filterSym(it, field, pm):
+            outputs.add(symToSuggest(it, isLocal=false, $ideSug, 100, pm, c.inTypeContext > 0, -99))
     else:
       # fallback:
       suggestEverything(c, n, field, outputs)
@@ -313,7 +379,7 @@ proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var int) =
       t = t.sons[0]
     suggestOperations(c, n, field, typ, outputs)
   else:
-    let orig = skipTypes(typ, {tyGenericInst, tyAlias})
+    let orig = typ # skipTypes(typ, {tyGenericInst, tyAlias})
     typ = skipTypes(typ, {tyGenericInst, tyVar, tyPtr, tyRef, tyAlias})
     if typ.kind == tyObject:
       var t = typ
@@ -384,17 +450,16 @@ when defined(nimsuggest):
     s.allUsages.add(info)
 
 var
-  #usageSym*: PSym
   lastLineInfo*: TLineInfo
 
 proc findUsages(info: TLineInfo; s: PSym; usageSym: var PSym) =
   if suggestVersion < 2:
     if usageSym == nil and isTracked(info, s.name.s.len):
       usageSym = s
-      suggestResult(symToSuggest(s, isLocal=false, $ideUse, 100))
+      suggestResult(symToSuggest(s, isLocal=false, $ideUse, 100, PrefixMatch.None, false, 0))
     elif s == usageSym:
       if lastLineInfo != info:
-        suggestResult(symToSuggest(s, isLocal=false, $ideUse, info, 100))
+        suggestResult(symToSuggest(s, isLocal=false, $ideUse, info, 100, PrefixMatch.None, false, 0))
       lastLineInfo = info
 
 when defined(nimsuggest):
@@ -402,12 +467,12 @@ when defined(nimsuggest):
     #echo "usages ", len(s.allUsages)
     for info in s.allUsages:
       let x = if info == s.info and info.col == s.info.col: "def" else: "use"
-      suggestResult(symToSuggest(s, isLocal=false, x, info, 100))
+      suggestResult(symToSuggest(s, isLocal=false, x, info, 100, PrefixMatch.None, false, 0))
 
 proc findDefinition(info: TLineInfo; s: PSym) =
   if s.isNil: return
   if isTracked(info, s.name.s.len):
-    suggestResult(symToSuggest(s, isLocal=false, $ideDef, 100))
+    suggestResult(symToSuggest(s, isLocal=false, $ideDef, 100, PrefixMatch.None, false, 0))
     suggestQuit()
 
 proc ensureIdx[T](x: var T, y: int) =
@@ -431,13 +496,13 @@ proc suggestSym*(info: TLineInfo; s: PSym; usageSym: var PSym; isDecl=true) {.in
       findDefinition(info, s)
     elif gIdeCmd == ideDus and s != nil:
       if isTracked(info, s.name.s.len):
-        suggestResult(symToSuggest(s, isLocal=false, $ideDef, 100))
+        suggestResult(symToSuggest(s, isLocal=false, $ideDef, 100, PrefixMatch.None, false, 0))
       findUsages(info, s, usageSym)
     elif gIdeCmd == ideHighlight and info.fileIndex == gTrackPos.fileIndex:
-      suggestResult(symToSuggest(s, isLocal=false, $ideHighlight, info, 100))
+      suggestResult(symToSuggest(s, isLocal=false, $ideHighlight, info, 100, PrefixMatch.None, false, 0))
     elif gIdeCmd == ideOutline and info.fileIndex == gTrackPos.fileIndex and
         isDecl:
-      suggestResult(symToSuggest(s, isLocal=false, $ideOutline, info, 100))
+      suggestResult(symToSuggest(s, isLocal=false, $ideOutline, info, 100, PrefixMatch.None, false, 0))
 
 proc markUsed(info: TLineInfo; s: PSym; usageSym: var PSym) =
   incl(s.flags, sfUsed)
@@ -464,11 +529,11 @@ proc suggestExpr*(c: PContext, node: PNode) =
   if gTrackPos.line < 0: return
   var cp = inCheckpoint(node.info)
   if cp == cpNone: return
-  var outputs = 0
   # This keeps semExpr() from coming here recursively:
   if c.compilesContextId > 0: return
   inc(c.compilesContextId)
 
+  var outputs: Suggestions = @[]
   if gIdeCmd == ideSug:
     var n = findClosestDot(node)
     if n == nil: n = node
@@ -507,7 +572,9 @@ proc suggestExpr*(c: PContext, node: PNode) =
       suggestCall(c, a, n, outputs)
 
   dec(c.compilesContextId)
-  if outputs > 0 and gIdeCmd in {ideSug, ideCon, ideDef}: suggestQuit()
+  if outputs.len > 0 and gIdeCmd in {ideSug, ideCon, ideDef}:
+    produceOutput(outputs)
+    suggestQuit()
 
 proc suggestStmt*(c: PContext, n: PNode) =
   suggestExpr(c, n)
@@ -518,10 +585,15 @@ proc suggestSentinel*(c: PContext) =
   inc(c.compilesContextId)
   # suggest everything:
   var isLocal = true
+  var outputs: Suggestions = @[]
+  var scopeN = 0
   for scope in walkScopes(c.currentScope):
     if scope == c.topLevelScope: isLocal = false
+    dec scopeN
     for it in items(scope.symbols):
-      if filterSymNoOpr(it, nil):
-        suggestResult(symToSuggest(it, isLocal = isLocal, $ideSug, 0))
+      var pm: PrefixMatch
+      if filterSymNoOpr(it, nil, pm):
+        outputs.add(symToSuggest(it, isLocal = isLocal, $ideSug, 0, PrefixMatch.None, false, scopeN))
 
+  produceOutput(outputs)
   dec(c.compilesContextId)
diff --git a/tools/nimsuggest/nimsuggest.nim b/tools/nimsuggest/nimsuggest.nim
index aced955b4..1798ac4e9 100644
--- a/tools/nimsuggest/nimsuggest.nim
+++ b/tools/nimsuggest/nimsuggest.nim
@@ -17,7 +17,7 @@ import compiler / [options, commands, modules, sem,
   passes, passaux, msgs, nimconf,
   extccomp, condsyms,
   sigmatch, ast, scriptconfig,
-  idents, modulegraphs, vm]
+  idents, modulegraphs, vm, prefixmatches]
 
 when defined(windows):
   import winlean
@@ -51,6 +51,11 @@ are supported.
 """
 type
   Mode = enum mstdin, mtcp, mepc, mcmdline
+  CachedMsg = object
+    info: TLineInfo
+    msg: string
+    sev: Severity
+  CachedMsgs = seq[CachedMsg]
 
 var
   gPort = 6000.Port
@@ -93,7 +98,7 @@ proc parseQuoted(cmd: string; outp: var string; start: int): int =
     i += parseUntil(cmd, outp, seps, i)
   result = i
 
-proc sexp(s: IdeCmd|TSymKind): SexpNode = sexp($s)
+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
@@ -110,6 +115,8 @@ proc sexp(s: Suggest): SexpNode =
     s.doc,
     s.quality
   ])
+  if s.section == ideSug:
+    result.add convertSexp(s.prefix)
 
 proc sexp(s: seq[Suggest]): SexpNode =
   result = newSList()
@@ -155,6 +162,9 @@ proc execute(cmd: IdeCmd, file, dirtyfile: string, line, col: int;
   if cmd == ideChk:
     msgs.structuredErrorHook = errorHook
     msgs.writelnHook = proc (s: string) = discard
+  else:
+    msgs.structuredErrorHook = nil
+    msgs.writelnHook = proc (s: string) = discard
   if cmd == ideUse and suggestVersion != 2:
     graph.resetAllModules()
   var isKnownFile = true
@@ -360,7 +370,7 @@ proc replEpc(x: ThreadParams) {.thread.} =
                          "unexpected call: " & epcAPI
       quit errMessage
 
-proc execCmd(cmd: string; graph: ModuleGraph; cache: IdentCache) =
+proc execCmd(cmd: string; graph: ModuleGraph; cache: IdentCache; cachedMsgs: CachedMsgs) =
   template sentinel() =
     # send sentinel for the input reading thread:
     results.send(Suggest(section: ideNone))
@@ -412,17 +422,19 @@ proc execCmd(cmd: string; graph: ModuleGraph; cache: IdentCache) =
   if gIdeCmd == ideKnown:
     results.send(Suggest(section: ideKnown, quality: ord(fileInfoKnown(orig))))
   else:
+    if gIdeCmd == ideChk:
+      for cm in cachedMsgs: errorHook(cm.info, cm.msg, cm.sev)
     execute(gIdeCmd, orig, dirtyfile, line, col-1, graph, cache)
   sentinel()
 
 proc recompileFullProject(graph: ModuleGraph; cache: IdentCache) =
-  echo "recompiling full project"
+  #echo "recompiling full project"
   resetSystemArtifacts()
   vm.globalCtx = nil
   graph.resetAllModules()
   GC_fullcollect()
   compileProject(graph, cache)
-  echo GC_getStatistics()
+  #echo GC_getStatistics()
 
 proc mainThread(graph: ModuleGraph; cache: IdentCache) =
   if gLogging:
@@ -439,12 +451,13 @@ proc mainThread(graph: ModuleGraph; cache: IdentCache) =
   suggestionResultHook = sugResultHook
   graph.doStopCompile = proc (): bool = requests.peek() > 0
   var idle = 0
+  var cachedMsgs: CachedMsgs = @[]
   while true:
     let (hasData, req) = requests.tryRecv()
     if hasData:
       msgs.writelnHook = wrHook
       suggestionResultHook = sugResultHook
-      execCmd(req, graph, cache)
+      execCmd(req, graph, cache, cachedMsgs)
       idle = 0
     else:
       os.sleep 250
@@ -453,7 +466,9 @@ proc mainThread(graph: ModuleGraph; cache: IdentCache) =
       # we use some nimsuggest activity to enable a lazy recompile:
       gIdeCmd = ideChk
       msgs.writelnHook = proc (s: string) = discard
-      msgs.structuredErrorHook = nil
+      cachedMsgs.setLen 0
+      msgs.structuredErrorHook = proc (info: TLineInfo; msg: string; sev: Severity) =
+        cachedMsgs.add(CachedMsg(info: info, msg: msg, sev: sev))
       suggestionResultHook = proc (s: Suggest) = discard
       recompileFullProject(graph, cache)
 
@@ -522,8 +537,13 @@ proc processCmdLine*(pass: TCmdLinePass, cmd: string) =
         suggestVersion = 2
         gMode = mstdin
         gEmitEof = true
+        gRefresh = false
       of "log": gLogging = true
-      of "refresh": gRefresh = true
+      of "refresh":
+        if p.val.len > 0:
+          gRefresh = parseBool(p.val)
+        else:
+          gRefresh = true
       else: processSwitch(pass, p)
     of cmdArgument:
       options.gProjectName = unixToNativePath(p.key)
diff --git a/tools/nimsuggest/tester.nim b/tools/nimsuggest/tester.nim
index 5a2933a6b..0bee14254 100644
--- a/tools/nimsuggest/tester.nim
+++ b/tools/nimsuggest/tester.nim
@@ -20,6 +20,7 @@ template tpath(): untyped = getAppDir() / "tests"
 proc parseTest(filename: string; epcMode=false): Test =
   const cursorMarker = "#[!]#"
   let nimsug = curDir & addFileExt("nimsuggest", ExeExt)
+  let libpath = findExe("nim").splitFile().dir /../ "lib"
   result.dest = getTempDir() / extractFilename(filename)
   result.cmd = nimsug & " --tester " & result.dest
   result.script = @[]
@@ -42,7 +43,7 @@ proc parseTest(filename: string; epcMode=false): Test =
       inc specSection
     elif specSection == 1:
       if x.startsWith("$nimsuggest"):
-        result.cmd = x % ["nimsuggest", nimsug, "file", filename]
+        result.cmd = x % ["nimsuggest", nimsug, "file", filename, "lib", libpath]
       elif x.startsWith("!"):
         if result.cmd.len == 0:
           result.startup.add x
@@ -54,7 +55,7 @@ proc parseTest(filename: string; epcMode=false): Test =
         result.script.add((x.substr(1).replaceWord("$path", tpath()), ""))
       elif x.len > 0:
         # expected output line:
-        let x = x % ["file", filename]
+        let x = x % ["file", filename, "lib", libpath]
         result.script[^1][1].add x.replace(";;", "\t") & '\L'
         # else: ignore empty lines for better readability of the specs
     inc i
@@ -164,7 +165,6 @@ proc sexpToAnswer(s: SexpNode): string =
   doAssert m.kind == SList
   for a in m:
     doAssert a.kind == SList
-    var first = true
     #s.section,
     #s.symkind,
     #s.qualifiedPath.map(newSString),
@@ -204,6 +204,9 @@ proc sexpToAnswer(s: SexpNode): string =
       result.add doc
       result.add '\t'
       result.add a[8].getNum
+      if a.len >= 10:
+        result.add '\t'
+        result.add a[9].getStr
     result.add '\L'
 
 proc doReport(filename, answer, resp: string; report: var string) =
@@ -300,9 +303,11 @@ proc runTest(filename: string): int =
 
 proc main() =
   var failures = 0
-  when false:
-    let x = getAppDir() / "tests/tchk1.nim"
+  if os.paramCount() > 0:
+    let f = os.paramStr(1)
+    let x = getAppDir() / f
     let xx = expandFilename x
+    failures += runTest(xx)
     failures += runEpcTest(xx)
   else:
     for x in walkFiles(getAppDir() / "tests/t*.nim"):
diff --git a/tools/nimsuggest/tests/tdot1.nim b/tools/nimsuggest/tests/tdot1.nim
index bcd44cd84..d31085f9d 100644
--- a/tools/nimsuggest/tests/tdot1.nim
+++ b/tools/nimsuggest/tests/tdot1.nim
@@ -1,9 +1,9 @@
 discard """
 $nimsuggest --tester $file
 >sug $1
-sug;;skField;;x;;int;;$file;;11;;4;;"";;100
-sug;;skField;;y;;int;;$file;;11;;7;;"";;100
-sug;;skProc;;tdot1.main;;proc (f: Foo);;$file;;13;;5;;"";;100
+sug;;skField;;x;;int;;$file;;11;;4;;"";;100;;None
+sug;;skField;;y;;int;;$file;;11;;7;;"";;100;;None
+sug;;skProc;;tdot1.main;;proc (f: Foo);;$file;;13;;5;;"";;100;;None
 """
 
 type
diff --git a/tools/nimsuggest/tests/tdot2.nim b/tools/nimsuggest/tests/tdot2.nim
index a58ac818b..f02b5cf16 100644
--- a/tools/nimsuggest/tests/tdot2.nim
+++ b/tools/nimsuggest/tests/tdot2.nim
@@ -17,13 +17,13 @@ proc main(f: Foo) =
 discard """
 $nimsuggest --tester $file
 >sug $1
-sug;;skField;;x;;int;;$file;;8;;4;;"";;100
-sug;;skField;;y;;int;;$file;;8;;7;;"";;100
-sug;;skProc;;tdot2.main;;proc (f: Foo);;$file;;12;;5;;"";;100
+sug;;skField;;x;;int;;$file;;8;;4;;"";;100;;None
+sug;;skField;;y;;int;;$file;;8;;7;;"";;100;;None
+sug;;skProc;;tdot2.main;;proc (f: Foo);;$file;;12;;5;;"";;100;;None
 !edit 0i32 1i32
 >sug $1
-sug;;skField;;x;;int;;$file;;8;;4;;"";;100
-sug;;skField;;y;;int;;$file;;8;;7;;"";;100
-sug;;skField;;z;;string;;$file;;10;;6;;"";;100
-sug;;skProc;;tdot2.main;;proc (f: Foo);;$file;;12;;5;;"";;100
+sug;;skField;;x;;int;;$file;;8;;4;;"";;100;;None
+sug;;skField;;y;;int;;$file;;8;;7;;"";;100;;None
+sug;;skField;;z;;string;;$file;;10;;6;;"";;100;;None
+sug;;skProc;;tdot2.main;;proc (f: Foo);;$file;;12;;5;;"";;100;;None
 """
diff --git a/tools/nimsuggest/tests/tdot3.nim b/tools/nimsuggest/tests/tdot3.nim
index 5badde867..15fc1cd1c 100644
--- a/tools/nimsuggest/tests/tdot3.nim
+++ b/tools/nimsuggest/tests/tdot3.nim
@@ -12,16 +12,16 @@ discard """
 !copy dep_v1.nim dep.nim
 $nimsuggest --tester $file
 >sug $1
-sug;;skField;;x;;int;;*dep.nim;;8;;4;;"";;100
-sug;;skField;;y;;int;;*dep.nim;;8;;8;;"";;100
-sug;;skProc;;tdot3.main;;proc (f: Foo);;$file;;5;;5;;"";;100
+sug;;skField;;x;;int;;*dep.nim;;8;;4;;"";;100;;None
+sug;;skField;;y;;int;;*dep.nim;;8;;8;;"";;100;;None
+sug;;skProc;;tdot3.main;;proc (f: Foo);;$file;;5;;5;;"";;100;;None
 
 !copy dep_v2.nim dep.nim
 >mod $path/dep.nim
 >sug $1
-sug;;skField;;x;;int;;*dep.nim;;8;;4;;"";;100
-sug;;skField;;y;;int;;*dep.nim;;8;;8;;"";;100
-sug;;skField;;z;;string;;*dep.nim;;9;;4;;"";;100
-sug;;skProc;;tdot3.main;;proc (f: Foo);;$file;;5;;5;;"";;100
+sug;;skField;;x;;int;;*dep.nim;;8;;4;;"";;100;;None
+sug;;skField;;y;;int;;*dep.nim;;8;;8;;"";;100;;None
+sug;;skField;;z;;string;;*dep.nim;;9;;4;;"";;100;;None
+sug;;skProc;;tdot3.main;;proc (f: Foo);;$file;;5;;5;;"";;100;;None
 !del dep.nim
 """
diff --git a/tools/nimsuggest/tests/tno_deref.nim b/tools/nimsuggest/tests/tno_deref.nim
index 0f8ba3bd8..05cffa507 100644
--- a/tools/nimsuggest/tests/tno_deref.nim
+++ b/tools/nimsuggest/tests/tno_deref.nim
@@ -9,6 +9,6 @@ x.#[!]#
 discard """
 $nimsuggest --tester $file
 >sug $1
-sug;;skProc;;tno_deref.foo;;proc (y: ptr int)*;;$file;;4;;5;;"";;100
+sug;;skProc;;tno_deref.foo;;proc (y: ptr int)*;;$file;;4;;5;;"";;100;;None
 *
 """
diff --git a/tools/nimsuggest/tests/tsug_regression.nim b/tools/nimsuggest/tests/tsug_regression.nim
new file mode 100644
index 000000000..1e440db2d
--- /dev/null
+++ b/tools/nimsuggest/tests/tsug_regression.nim
@@ -0,0 +1,28 @@
+# test we only get suggestions, not error messages:
+
+import tables, sets, parsecfg
+
+type X = object
+
+proc main =
+  # bug #52
+  var
+    set0 = initSet[int]()
+    set1 = initSet[X]()
+    set2 = initSet[ref int]()
+
+    map0 = initTable[int, int]()
+    map1 = initOrderedTable[string, int]()
+    cfg = loadConfig("file")
+  map0.#[!]#
+
+discard """
+$nimsuggest --tester $file
+>sug $1
+sug;;skProc;;tables.getOrDefault;;proc (t: Table[getOrDefault.A, getOrDefault.B], key: A): B;;$lib/pure/collections/tables.nim;;178;;5;;"";;100;;None
+sug;;skProc;;tables.hasKey;;proc (t: Table[hasKey.A, hasKey.B], key: A): bool;;$lib/pure/collections/tables.nim;;233;;5;;"returns true iff `key` is in the table `t`.";;100;;None
+sug;;skProc;;tables.add;;proc (t: var Table[add.A, add.B], key: A, val: B);;$lib/pure/collections/tables.nim;;297;;5;;"puts a new (key, value)-pair into `t` even if ``t[key]`` already exists.";;100;;None
+sug;;skIterator;;tables.allValues;;iterator (t: Table[allValues.A, allValues.B], key: A): B{.inline.};;$lib/pure/collections/tables.nim;;225;;9;;"iterates over any value in the table `t` that belongs to the given `key`.";;100;;None
+sug;;skProc;;tables.clear;;proc (t: var Table[clear.A, clear.B]);;$lib/pure/collections/tables.nim;;121;;5;;"Resets the table so that it is empty.";;100;;None
+*
+"""
diff --git a/tools/nimsuggest/tests/twithin_macro.nim b/tools/nimsuggest/tests/twithin_macro.nim
index 7392dd605..e0df03542 100644
--- a/tools/nimsuggest/tests/twithin_macro.nim
+++ b/tools/nimsuggest/tests/twithin_macro.nim
@@ -204,10 +204,10 @@ echo r
 discard """
 $nimsuggest --tester $file
 >sug $1
-sug;;skField;;name;;string;;$file;;166;;6;;"";;100
-sug;;skField;;age;;int;;$file;;167;;6;;"";;100
-sug;;skMethod;;twithin_macro.age_human_yrs;;proc (self: Animal): int;;$file;;169;;9;;"";;100
-sug;;skMacro;;twithin_macro.class;;proc (head: untyped, body: untyped): untyped{.gcsafe, locks: <unknown>.};;$file;;4;;6;;"Iterates over the children of the NimNode ``n``.";;100
-sug;;skMethod;;twithin_macro.vocalize;;proc (self: Animal): string;;$file;;168;;9;;"";;100
-sug;;skMethod;;twithin_macro.vocalize;;proc (self: Rabbit): string;;$file;;184;;9;;"";;100*
+sug;;skField;;age;;int;;$file;;167;;6;;"";;100;;None
+sug;;skField;;name;;string;;$file;;166;;6;;"";;100;;None
+sug;;skMethod;;twithin_macro.age_human_yrs;;proc (self: Animal): int;;$file;;169;;9;;"";;100;;None
+sug;;skMethod;;twithin_macro.vocalize;;proc (self: Animal): string;;$file;;168;;9;;"";;100;;None
+sug;;skMethod;;twithin_macro.vocalize;;proc (self: Rabbit): string;;$file;;184;;9;;"";;100;;None
+sug;;skMacro;;twithin_macro.class;;proc (head: untyped, body: untyped): untyped{.gcsafe, locks: <unknown>.};;$file;;4;;6;;"";;50;;None*
 """
diff --git a/tools/nimsuggest/tests/twithin_macro_prefix.nim b/tools/nimsuggest/tests/twithin_macro_prefix.nim
index 6ee9fb2dc..86e406c5d 100644
--- a/tools/nimsuggest/tests/twithin_macro_prefix.nim
+++ b/tools/nimsuggest/tests/twithin_macro_prefix.nim
@@ -204,6 +204,6 @@ echo r
 discard """
 $nimsuggest --tester $file
 >sug $1
-sug;;skField;;age;;int;;$file;;167;;6;;"";;100
-sug;;skMethod;;twithin_macro_prefix.age_human_yrs;;proc (self: Animal): int;;$file;;169;;9;;"";;100
+sug;;skField;;age;;int;;$file;;167;;6;;"";;100;;Prefix
+sug;;skMethod;;twithin_macro_prefix.age_human_yrs;;proc (self: Animal): int;;$file;;169;;9;;"";;100;;Prefix
 """