summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/ast.nim8
-rw-r--r--compiler/lexer.nim6
-rw-r--r--compiler/options.nim14
-rw-r--r--compiler/parser.nim70
-rw-r--r--compiler/semexprs.nim30
-rw-r--r--compiler/sempass2.nim3
-rw-r--r--compiler/semstmts.nim2
-rw-r--r--compiler/suggest.nim12
-rw-r--r--compiler/vm.nim3
-rw-r--r--nimsuggest/nimsuggest.nim193
-rw-r--r--nimsuggest/tester.nim15
-rw-r--r--nimsuggest/tests/tv3.nim4
-rw-r--r--nimsuggest/tests/tv3_outline.nim45
13 files changed, 345 insertions, 60 deletions
diff --git a/compiler/ast.nim b/compiler/ast.nim
index aa0ca98c9..aa70560ed 100644
--- a/compiler/ast.nim
+++ b/compiler/ast.nim
@@ -796,6 +796,8 @@ type
       ident*: PIdent
     else:
       sons*: TNodeSeq
+    when defined(nimsuggest):
+      endInfo*: TLineInfo
 
   TStrTable* = object         # a table[PIdent] of PSym
     counter*: int
@@ -892,6 +894,8 @@ type
     typ*: PType
     name*: PIdent
     info*: TLineInfo
+    when defined(nimsuggest):
+      endInfo*: TLineInfo
     owner*: PSym
     flags*: TSymFlags
     ast*: PNode               # syntax tree of proc, iterator, etc.:
@@ -1690,6 +1694,8 @@ proc copyNode*(src: PNode): PNode =
   of nkIdent: result.ident = src.ident
   of nkStrLit..nkTripleStrLit: result.strVal = src.strVal
   else: discard
+  when defined(nimsuggest):
+    result.endInfo = src.endInfo
 
 template transitionNodeKindCommon(k: TNodeKind) =
   let obj {.inject.} = n[]
@@ -1742,6 +1748,8 @@ template copyNodeImpl(dst, src, processSonsStmt) =
   if src == nil: return
   dst = newNode(src.kind)
   dst.info = src.info
+  when defined(nimsuggest):
+    result.endInfo = src.endInfo
   dst.typ = src.typ
   dst.flags = src.flags * PersistentNodeFlags
   dst.comment = src.comment
diff --git a/compiler/lexer.nim b/compiler/lexer.nim
index ec27add8a..a62d40e54 100644
--- a/compiler/lexer.nim
+++ b/compiler/lexer.nim
@@ -127,6 +127,8 @@ type
     cache*: IdentCache
     when defined(nimsuggest):
       previousToken: TLineInfo
+      tokenEnd*: TLineInfo
+      previousTokenEnd*: TLineInfo
     config*: ConfigRef
 
 proc getLineInfo*(L: Lexer, tok: Token): TLineInfo {.inline.} =
@@ -1224,6 +1226,10 @@ proc skip(L: var Lexer, tok: var Token) =
 proc rawGetTok*(L: var Lexer, tok: var Token) =
   template atTokenEnd() {.dirty.} =
     when defined(nimsuggest):
+      L.previousTokenEnd.line = L.tokenEnd.line
+      L.previousTokenEnd.col = L.tokenEnd.col
+      L.tokenEnd.line = tok.line.uint16
+      L.tokenEnd.col = getColNumber(L, L.bufpos).int16
       # we attach the cursor to the last *strong* token
       if tok.tokType notin weakTokens:
         L.previousToken.line = tok.line.uint16
diff --git a/compiler/options.nim b/compiler/options.nim
index fc970e420..8c477f0a5 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -195,7 +195,7 @@ type
   IdeCmd* = enum
     ideNone, ideSug, ideCon, ideDef, ideUse, ideDus, ideChk, ideChkFile, ideMod,
     ideHighlight, ideOutline, ideKnown, ideMsg, ideProject, ideGlobalSymbols,
-    ideRecompile, ideChanged, ideType, ideDeclaration
+    ideRecompile, ideChanged, ideType, ideDeclaration, ideExpand
 
   Feature* = enum  ## experimental features; DO NOT RENAME THESE!
     dotOperators,
@@ -278,6 +278,9 @@ type
     scope*, localUsages*, globalUsages*: int # more usages is better
     tokenLen*: int
     version*: int
+    endLine*: uint16
+    endCol*: int
+
   Suggestions* = seq[Suggest]
 
   ProfileInfo* = object
@@ -408,6 +411,11 @@ type
     nimMainPrefix*: string
     vmProfileData*: ProfileData
 
+    expandProgress*: bool
+    expandLevels*: int
+    expandNodeResult*: string
+    expandPosition*: TLineInfo
+
 proc parseNimVersion*(a: string): NimVer =
   # could be moved somewhere reusable
   if a.len > 0:
@@ -996,6 +1004,9 @@ proc isDynlibOverride*(conf: ConfigRef; lib: string): bool =
   result = optDynlibOverrideAll in conf.globalOptions or
      conf.dllOverrides.hasKey(lib.canonDynlibName)
 
+proc expandDone*(conf: ConfigRef): bool =
+  result = conf.ideCmd == ideExpand and conf.expandLevels == 0 and conf.expandProgress
+
 proc parseIdeCmd*(s: string): IdeCmd =
   case s:
   of "sug": ideSug
@@ -1035,6 +1046,7 @@ proc `$`*(c: IdeCmd): string =
   of ideProject: "project"
   of ideGlobalSymbols: "globalSymbols"
   of ideDeclaration: "declaration"
+  of ideExpand: "expand"
   of ideRecompile: "recompile"
   of ideChanged: "changed"
   of ideType: "type"
diff --git a/compiler/parser.nim b/compiler/parser.nim
index 0c8edd2a8..14882d415 100644
--- a/compiler/parser.nim
+++ b/compiler/parser.nim
@@ -354,6 +354,12 @@ proc colcom(p: var Parser, n: PNode) =
 
 const tkBuiltInMagics = {tkType, tkStatic, tkAddr}
 
+template setEndInfo() =
+  when defined(nimsuggest):
+    result.endInfo = TLineInfo(fileIndex: p.lex.fileIdx,
+                     line: p.lex.previousTokenEnd.line,
+                     col: p.lex.previousTokenEnd.col)
+
 proc parseSymbol(p: var Parser, mode = smNormal): PNode =
   #| symbol = '`' (KEYW|IDENT|literal|(operator|'('|')'|'['|']'|'{'|'}'|'=')+)+ '`'
   #|        | IDENT | KEYW
@@ -406,6 +412,7 @@ proc parseSymbol(p: var Parser, mode = smNormal): PNode =
     # if it is a keyword:
     #if not isKeyword(p.tok.tokType): getTok(p)
     result = p.emptyNode
+  setEndInfo()
 
 proc equals(p: var Parser, a: PNode): PNode =
   if p.tok.tokType == tkEquals:
@@ -577,6 +584,7 @@ proc parseCast(p: var Parser): PNode =
     result.add(exprColonEqExpr(p))
   optPar(p)
   eat(p, tkParRi)
+  setEndInfo()
 
 proc setBaseFlags(n: PNode, base: NumericalBase) =
   case base
@@ -599,6 +607,7 @@ proc parseGStrLit(p: var Parser, a: PNode): PNode =
     getTok(p)
   else:
     result = a
+  setEndInfo()
 
 proc complexOrSimpleStmt(p: var Parser): PNode
 proc simpleExpr(p: var Parser, mode = pmNormal): PNode
@@ -703,6 +712,7 @@ proc parsePar(p: var Parser): PNode =
           skipComment(p, a)
   optPar(p)
   eat(p, tkParRi)
+  setEndInfo()
 
 proc identOrLiteral(p: var Parser, mode: PrimaryMode): PNode =
   #| literal = | INT_LIT | INT8_LIT | INT16_LIT | INT32_LIT | INT64_LIT
@@ -941,6 +951,7 @@ proc parseOperators(p: var Parser, headNode: PNode,
     a.add(b)
     result = a
     opPrec = getPrecedence(p.tok)
+  setEndInfo()
 
 proc simpleExprAux(p: var Parser, limit: int, mode: PrimaryMode): PNode =
   var mode = mode
@@ -990,6 +1001,7 @@ proc parsePragma(p: var Parser): PNode =
   when defined(nimpretty):
     dec p.em.doIndentMore
     dec p.em.keepIndents
+  setEndInfo()
 
 proc identVis(p: var Parser; allowDot=false): PNode =
   #| identVis = symbol OPR?  # postfix position
@@ -1058,6 +1070,7 @@ proc parseIdentColonEquals(p: var Parser, flags: DeclaredIdentFlags): PNode =
     result.add(parseExpr(p))
   else:
     result.add(newNodeP(nkEmpty, p))
+  setEndInfo()
 
 proc parseTuple(p: var Parser, indentAllowed = false): PNode =
   #| tupleTypeBracket = '[' optInd (identColonEquals (comma/semicolon)?)* optPar ']'
@@ -1102,6 +1115,7 @@ proc parseTuple(p: var Parser, indentAllowed = false): PNode =
     parMessage(p, errGenerated, "the syntax for tuple types is 'tuple[...]', not 'tuple(...)'")
   else:
     result = newNodeP(nkTupleClassTy, p)
+  setEndInfo()
 
 proc parseParamList(p: var Parser, retColon = true): PNode =
   #| paramList = '(' declColonEquals ^* (comma/semicolon) ')'
@@ -1150,6 +1164,7 @@ proc parseParamList(p: var Parser, retColon = true): PNode =
   when defined(nimpretty):
     dec p.em.doIndentMore
     dec p.em.keepIndents
+  setEndInfo()
 
 proc optPragmas(p: var Parser): PNode =
   if p.tok.tokType == tkCurlyDotLe and (p.tok.indent < 0 or realInd(p)):
@@ -1170,6 +1185,7 @@ proc parseDoBlock(p: var Parser; info: TLineInfo): PNode =
     result = newProcNode(nkDo, info,
       body = result, params = params, name = p.emptyNode, pattern = p.emptyNode,
       genericParams = p.emptyNode, pragmas = pragmas, exceptions = p.emptyNode)
+  setEndInfo()
 
 proc parseProcExpr(p: var Parser; isExpr: bool; kind: TNodeKind): PNode =
   #| routineExpr = ('proc' | 'func' | 'iterator') paramListColon pragma? ('=' COMMENT? stmt)?
@@ -1192,6 +1208,7 @@ proc parseProcExpr(p: var Parser; isExpr: bool; kind: TNodeKind): PNode =
       if kind == nkFuncDef:
         parMessage(p, "func keyword is not allowed in type descriptions, use proc with {.noSideEffect.} pragma instead")
       result.add(pragmas)
+  setEndInfo()
 
 proc isExprStart(p: Parser): bool =
   case p.tok.tokType
@@ -1211,6 +1228,7 @@ proc parseSymbolList(p: var Parser, result: PNode) =
     if p.tok.tokType != tkComma: break
     getTok(p)
     optInd(p, s)
+  setEndInfo()
 
 proc parseTypeDescKAux(p: var Parser, kind: TNodeKind,
                        mode: PrimaryMode): PNode =
@@ -1239,6 +1257,7 @@ proc parseTypeDescKAux(p: var Parser, kind: TNodeKind,
     parseSymbolList(p, list)
   if mode == pmTypeDef and not isTypedef:
     result = parseOperators(p, result, -1, mode)
+  setEndInfo()
 
 proc parseVarTuple(p: var Parser): PNode
 
@@ -1264,6 +1283,7 @@ proc parseFor(p: var Parser): PNode =
   result.add(parseExpr(p))
   colcom(p, result)
   result.add(parseStmt(p))
+  setEndInfo()
 
 template nimprettyDontTouch(body) =
   when defined(nimpretty):
@@ -1302,6 +1322,7 @@ proc parseExpr(p: var Parser): PNode =
     nimprettyDontTouch:
       result = parseTry(p, isExpr=true)
   else: result = simpleExpr(p)
+  setEndInfo()
 
 proc parseEnum(p: var Parser): PNode
 proc parseObject(p: var Parser): PNode
@@ -1411,6 +1432,7 @@ proc parseTypeDesc(p: var Parser, fullExpr = false): PNode =
     else:
       result = simpleExpr(p, pmTypeDesc)
   result = binaryNot(p, result)
+  setEndInfo()
 
 proc parseTypeDefValue(p: var Parser): PNode =
   #| typeDefValue = ((tupleDecl | enumDecl | objectDecl | conceptDecl |
@@ -1441,6 +1463,7 @@ proc parseTypeDefValue(p: var Parser): PNode =
           result.add(commandParam(p, isFirstParam, pmTypeDef))
       result = postExprBlocks(p, result)
   result = binaryNot(p, result)
+  setEndInfo()
 
 proc makeCall(n: PNode): PNode =
   ## Creates a call if the given node isn't already a call.
@@ -1561,6 +1584,7 @@ proc parseExprStmt(p: var Parser): PNode =
     else:
       result = a
     result = postExprBlocks(p, result)
+  setEndInfo()
 
 proc parseModuleName(p: var Parser, kind: TNodeKind): PNode =
   result = parseExpr(p)
@@ -1572,6 +1596,7 @@ proc parseModuleName(p: var Parser, kind: TNodeKind): PNode =
       getTok(p)
       result.add(a)
       result.add(parseExpr(p))
+  setEndInfo()
 
 proc parseImport(p: var Parser, kind: TNodeKind): PNode =
   #| importStmt = 'import' optInd expr
@@ -1600,6 +1625,7 @@ proc parseImport(p: var Parser, kind: TNodeKind): PNode =
       getTok(p)
       optInd(p, a)
   #expectNl(p)
+  setEndInfo()
 
 proc parseIncludeStmt(p: var Parser): PNode =
   #| includeStmt = 'include' optInd expr ^+ comma
@@ -1616,6 +1642,7 @@ proc parseIncludeStmt(p: var Parser): PNode =
     getTok(p)
     optInd(p, a)
   #expectNl(p)
+  setEndInfo()
 
 proc parseFromStmt(p: var Parser): PNode =
   #| fromStmt = 'from' expr 'import' optInd expr (comma expr)*
@@ -1636,6 +1663,7 @@ proc parseFromStmt(p: var Parser): PNode =
     getTok(p)
     optInd(p, a)
   #expectNl(p)
+  setEndInfo()
 
 proc parseReturnOrRaise(p: var Parser, kind: TNodeKind): PNode =
   #| returnStmt = 'return' optInd expr?
@@ -1657,6 +1685,7 @@ proc parseReturnOrRaise(p: var Parser, kind: TNodeKind): PNode =
     var e = parseExpr(p)
     e = postExprBlocks(p, e)
     result.add(e)
+  setEndInfo()
 
 proc parseIfOrWhen(p: var Parser, kind: TNodeKind): PNode =
   #| condStmt = expr colcom stmt COMMENT?
@@ -1681,6 +1710,7 @@ proc parseIfOrWhen(p: var Parser, kind: TNodeKind): PNode =
     colcom(p, branch)
     branch.add(parseStmt(p))
     result.add(branch)
+  setEndInfo()
 
 proc parseIfOrWhenExpr(p: var Parser, kind: TNodeKind): PNode =
   #| condExpr = expr colcom expr optInd
@@ -1705,6 +1735,7 @@ proc parseIfOrWhenExpr(p: var Parser, kind: TNodeKind): PNode =
     colcom(p, branch)
     branch.add(parseStmt(p))
     result.add(branch)
+  setEndInfo()
 
 proc parseWhile(p: var Parser): PNode =
   #| whileStmt = 'while' expr colcom stmt
@@ -1714,6 +1745,7 @@ proc parseWhile(p: var Parser): PNode =
   result.add(parseExpr(p))
   colcom(p, result)
   result.add(parseStmt(p))
+  setEndInfo()
 
 proc parseCase(p: var Parser): PNode =
   #| ofBranch = 'of' exprList colcom stmt
@@ -1761,6 +1793,7 @@ proc parseCase(p: var Parser): PNode =
 
   if wasIndented:
     p.currInd = oldInd
+  setEndInfo()
 
 proc parseTry(p: var Parser; isExpr: bool): PNode =
   #| tryStmt = 'try' colcom stmt &(IND{=}? 'except'|'finally')
@@ -1789,12 +1822,14 @@ proc parseTry(p: var Parser; isExpr: bool): PNode =
     b.add(parseStmt(p))
     result.add(b)
   if b == nil: parMessage(p, "expected 'except'")
+  setEndInfo()
 
 proc parseExceptBlock(p: var Parser, kind: TNodeKind): PNode =
   result = newNodeP(kind, p)
   getTok(p)
   colcom(p, result)
   result.add(parseStmt(p))
+  setEndInfo()
 
 proc parseBlock(p: var Parser): PNode =
   #| blockStmt = 'block' symbol? colcom stmt
@@ -1805,6 +1840,7 @@ proc parseBlock(p: var Parser): PNode =
   else: result.add(parseSymbol(p))
   colcom(p, result)
   result.add(parseStmt(p))
+  setEndInfo()
 
 proc parseStaticOrDefer(p: var Parser; k: TNodeKind): PNode =
   #| staticStmt = 'static' colcom stmt
@@ -1813,6 +1849,7 @@ proc parseStaticOrDefer(p: var Parser; k: TNodeKind): PNode =
   getTok(p)
   colcom(p, result)
   result.add(parseStmt(p))
+  setEndInfo()
 
 proc parseAsm(p: var Parser): PNode =
   #| asmStmt = 'asm' pragma? (STR_LIT | RSTR_LIT | TRIPLESTR_LIT)
@@ -1829,6 +1866,7 @@ proc parseAsm(p: var Parser): PNode =
     result.add(p.emptyNode)
     return
   getTok(p)
+  setEndInfo()
 
 proc parseGenericParam(p: var Parser): PNode =
   #| genericParam = symbol (comma symbol)* (colon expr)? ('=' optInd expr)?
@@ -1864,6 +1902,7 @@ proc parseGenericParam(p: var Parser): PNode =
     result.add(parseExpr(p))
   else:
     result.add(p.emptyNode)
+  setEndInfo()
 
 proc parseGenericParamList(p: var Parser): PNode =
   #| genericParamList = '[' optInd
@@ -1882,12 +1921,14 @@ proc parseGenericParamList(p: var Parser): PNode =
     skipComment(p, a)
   optPar(p)
   eat(p, tkBracketRi)
+  setEndInfo()
 
 proc parsePattern(p: var Parser): PNode =
   #| pattern = '{' stmt '}'
   eat(p, tkCurlyLe)
   result = parseStmt(p)
   eat(p, tkCurlyRi)
+  setEndInfo()
 
 proc parseRoutine(p: var Parser, kind: TNodeKind): PNode =
   #| indAndComment = (IND{>} COMMENT)? | COMMENT?
@@ -1932,6 +1973,7 @@ proc parseRoutine(p: var Parser, kind: TNodeKind): PNode =
     #else:
     #  assert false, p.lex.config$body.info # avoids hard to track bugs, fail early.
     # Yeah, that worked so well. There IS a bug in this logic, now what?
+  setEndInfo()
 
 proc newCommentStmt(p: var Parser): PNode =
   #| commentStmt = COMMENT
@@ -1967,6 +2009,7 @@ proc parseSection(p: var Parser, kind: TNodeKind,
     result.add(defparser(p))
   else:
     parMessage(p, errIdentifierExpected, p.tok)
+  setEndInfo()
 
 proc parseEnum(p: var Parser): PNode =
   #| enumDecl = 'enum' optInd (symbol pragma? optInd ('=' optInd expr COMMENT?)? comma?)+
@@ -2013,6 +2056,7 @@ proc parseEnum(p: var Parser): PNode =
       break
   if result.len <= 1:
     parMessage(p, errIdentifierExpected, p.tok)
+  setEndInfo()
 
 proc parseObjectPart(p: var Parser): PNode
 proc parseObjectWhen(p: var Parser): PNode =
@@ -2038,6 +2082,7 @@ proc parseObjectWhen(p: var Parser): PNode =
     branch.add(parseObjectPart(p))
     flexComment(p, branch)
     result.add(branch)
+  setEndInfo()
 
 proc parseObjectCase(p: var Parser): PNode =
   #| objectBranch = 'of' exprList colcom objectPart
@@ -2079,6 +2124,7 @@ proc parseObjectCase(p: var Parser): PNode =
     if b.kind == nkElse: break
   if wasIndented:
     p.currInd = oldInd
+  setEndInfo()
 
 proc parseObjectPart(p: var Parser): PNode =
   #| objectPart = IND{>} objectPart^+IND{=} DED
@@ -2111,6 +2157,7 @@ proc parseObjectPart(p: var Parser): PNode =
       result = p.emptyNode
   else:
     result = p.emptyNode
+  setEndInfo()
 
 proc parseObject(p: var Parser): PNode =
   #| objectDecl = 'object' ('of' typeDesc)? COMMENT? objectPart
@@ -2131,6 +2178,7 @@ proc parseObject(p: var Parser): PNode =
     result.add(p.emptyNode)
   else:
     result.add(parseObjectPart(p))
+  setEndInfo()
 
 proc parseTypeClassParam(p: var Parser): PNode =
   let modifier =
@@ -2148,6 +2196,7 @@ proc parseTypeClassParam(p: var Parser): PNode =
     result.add(p.parseSymbol)
   else:
     result = p.parseSymbol
+  setEndInfo()
 
 proc parseTypeClass(p: var Parser): PNode =
   #| conceptParam = ('var' | 'out')? symbol
@@ -2191,6 +2240,7 @@ proc parseTypeClass(p: var Parser): PNode =
     result.add(p.emptyNode)
   else:
     result.add(parseStmt(p))
+  setEndInfo()
 
 proc parseTypeDef(p: var Parser): PNode =
   #|
@@ -2224,6 +2274,7 @@ proc parseTypeDef(p: var Parser): PNode =
   else:
     result.add(p.emptyNode)
   indAndComment(p, result)    # special extension!
+  setEndInfo()
 
 proc parseVarTuple(p: var Parser): PNode =
   #| varTuple = '(' optInd identWithPragma ^+ comma optPar ')' '=' optInd expr
@@ -2240,6 +2291,7 @@ proc parseVarTuple(p: var Parser): PNode =
   result.add(p.emptyNode)         # no type desc
   optPar(p)
   eat(p, tkParRi)
+  setEndInfo()
 
 proc parseVariable(p: var Parser): PNode =
   #| colonBody = colcom stmt postExprBlocks?
@@ -2252,6 +2304,7 @@ proc parseVariable(p: var Parser): PNode =
   else: result = parseIdentColonEquals(p, {withPragma, withDot})
   result[^1] = postExprBlocks(p, result[^1])
   indAndComment(p, result)
+  setEndInfo()
 
 proc parseConstant(p: var Parser): PNode =
   #| constant = (varTuple / identWithPragma) (colon typeDesc)? '=' optInd expr indAndComment
@@ -2271,6 +2324,7 @@ proc parseConstant(p: var Parser): PNode =
   result.add(parseExpr(p))
   result[^1] = postExprBlocks(p, result[^1])
   indAndComment(p, result)
+  setEndInfo()
 
 proc parseBind(p: var Parser, k: TNodeKind): PNode =
   #| bindStmt = 'bind' optInd qualifiedIdent ^+ comma
@@ -2286,6 +2340,7 @@ proc parseBind(p: var Parser, k: TNodeKind): PNode =
     getTok(p)
     optInd(p, a)
   #expectNl(p)
+  setEndInfo()
 
 proc parseStmtPragma(p: var Parser): PNode =
   #| pragmaStmt = pragma (':' COMMENT? stmt)?
@@ -2297,6 +2352,7 @@ proc parseStmtPragma(p: var Parser): PNode =
     skipComment(p, result)
     result.add a
     result.add parseStmt(p)
+  setEndInfo()
 
 proc simpleStmt(p: var Parser): PNode =
   #| simpleStmt = ((returnStmt | raiseStmt | yieldStmt | discardStmt | breakStmt
@@ -2439,6 +2495,7 @@ proc parseStmt(p: var Parser): PNode =
           if p.tok.tokType != tkSemiColon: break
           getTok(p)
           if err and p.tok.tokType == tkEof: break
+  setEndInfo()
 
 proc parseAll(p: var Parser): PNode =
   ## Parses the rest of the input stream held by the parser into a PNode.
@@ -2454,6 +2511,7 @@ proc parseAll(p: var Parser): PNode =
       getTok(p)
     if p.tok.indent != 0:
       parMessage(p, errInvalidIndentation)
+  setEndInfo()
 
 proc checkFirstLineIndentation*(p: var Parser) =
   if p.tok.indent != 0 and p.tok.strongSpaceA:
@@ -2487,6 +2545,7 @@ proc parseTopLevelStmt(p: var Parser): PNode =
       result = complexOrSimpleStmt(p)
       if result.kind == nkEmpty: parMessage(p, errExprExpected, p.tok)
       break
+  setEndInfo()
 
 proc parseString*(s: string; cache: IdentCache; config: ConfigRef;
                   filename: string = ""; line: int = 0;
@@ -2498,9 +2557,10 @@ proc parseString*(s: string; cache: IdentCache; config: ConfigRef;
   var stream = llStreamOpen(s)
   stream.lineOffset = line
 
-  var parser: Parser
-  parser.lex.errorHandler = errorHandler
-  openParser(parser, AbsoluteFile filename, stream, cache, config)
+  var p: Parser
+  p.lex.errorHandler = errorHandler
+  openParser(p, AbsoluteFile filename, stream, cache, config)
 
-  result = parser.parseAll
-  closeParser(parser)
+  result = p.parseAll
+  closeParser(p)
+  setEndInfo()
diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim
index 612af482a..7454c7f8e 100644
--- a/compiler/semexprs.nim
+++ b/compiler/semexprs.nim
@@ -980,6 +980,14 @@ proc afterCallActions(c: PContext; n, orig: PNode, flags: TExprFlags; expectedTy
     return errorNode(c, n)
 
   result = n
+
+  when defined(nimsuggest):
+    if c.config.expandProgress:
+      if c.config.expandLevels == 0:
+        return n
+      else:
+        c.config.expandLevels -= 1
+
   let callee = result[0].sym
   case callee.kind
   of skMacro: result = semMacroExpr(c, result, orig, callee, flags, expectedType)
@@ -1890,6 +1898,9 @@ proc semReturn(c: PContext, n: PNode): PNode =
     localError(c.config, n.info, "'return' not allowed here")
 
 proc semProcBody(c: PContext, n: PNode; expectedType: PType = nil): PNode =
+  when defined(nimsuggest):
+    if c.graph.config.expandDone():
+      return n
   openScope(c)
   result = semExpr(c, n, expectedType = expectedType)
   if c.p.resultSym != nil and not isEmptyType(result.typ):
@@ -2895,7 +2906,6 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
     defer:
       if isCompilerDebug():
         echo ("<", c.config$n.info, n, ?.result.typ)
-
   template directLiteral(typeKind: TTypeKind) =
     if result.typ == nil:
       if expectedType != nil and (
@@ -2907,6 +2917,19 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
         result.typ = getSysType(c.graph, n.info, typeKind)
 
   result = n
+  when defined(nimsuggest):
+    var expandStarted = false
+    if c.config.ideCmd == ideExpand and not c.config.expandProgress and
+        ((n.kind in {nkFuncDef, nkProcDef, nkIteratorDef, nkTemplateDef, nkMethodDef, nkConverterDef} and
+          n.info.exactEquals(c.config.expandPosition)) or
+         (n.kind in {nkCall, nkCommand} and
+          n[0].info.exactEquals(c.config.expandPosition))):
+      expandStarted = true
+      c.config.expandProgress = true
+      if c.config.expandLevels == 0:
+        c.config.expandNodeResult = $n
+        suggestQuit()
+
   if c.config.cmd == cmdIdeTools: suggestExpr(c, n)
   if nfSem in n.flags: return
   case n.kind
@@ -3234,3 +3257,8 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
     localError(c.config, n.info, "invalid expression: " &
                renderTree(n, {renderNoComments}))
   if result != nil: incl(result.flags, nfSem)
+
+  when defined(nimsuggest):
+    if expandStarted:
+      c.config.expandNodeResult = $result
+      suggestQuit()
diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim
index 747540c78..e989eb4a6 100644
--- a/compiler/sempass2.nim
+++ b/compiler/sempass2.nim
@@ -1450,6 +1450,9 @@ proc hasRealBody(s: PSym): bool =
 
 proc trackProc*(c: PContext; s: PSym, body: PNode) =
   let g = c.graph
+  when defined(nimsuggest):
+    if g.config.expandDone():
+      return
   var effects = s.typ.n[0]
   if effects.kind != nkEffectList: return
   # effects already computed?
diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim
index 96883255c..c7e48db4c 100644
--- a/compiler/semstmts.nim
+++ b/compiler/semstmts.nim
@@ -2156,7 +2156,7 @@ proc semProcAux(c: PContext, n: PNode, kind: TSymKind,
         if s.kind notin {skMacro, skTemplate} and s.magic == mNone: paramsTypeCheck(c, s.typ)
 
         maybeAddResult(c, s, n)
-        let resultType = 
+        let resultType =
           if s.kind == skMacro:
             sysTypeFromName(c.graph, n.info, "NimNode")
           elif not isInlineIterator(s.typ):
diff --git a/compiler/suggest.nim b/compiler/suggest.nim
index e0b0fb516..637010ad5 100644
--- a/compiler/suggest.nim
+++ b/compiler/suggest.nim
@@ -120,7 +120,9 @@ proc getTokenLenFromSource(conf: ConfigRef; ident: string; info: TLineInfo): int
 proc symToSuggest*(g: ModuleGraph; s: PSym, isLocal: bool, section: IdeCmd, info: TLineInfo;
                   quality: range[0..100]; prefix: PrefixMatch;
                   inTypeContext: bool; scope: int;
-                  useSuppliedInfo = false): Suggest =
+                  useSuppliedInfo = false,
+                  endLine: uint16 = 0,
+                  endCol = 0): Suggest =
   new(result)
   result.section = section
   result.quality = quality
@@ -176,6 +178,8 @@ proc symToSuggest*(g: ModuleGraph; s: PSym, isLocal: bool, section: IdeCmd, info
                       else:
                         getTokenLenFromSource(g.config, s.name.s, infox)
   result.version = g.config.suggestVersion
+  result.endLine = endLine
+  result.endCol = endCol
 
 proc `$`*(suggest: Suggest): string =
   result = $suggest.section
@@ -216,6 +220,12 @@ proc `$`*(suggest: Suggest): string =
         result.add(sep)
         result.add($suggest.prefix)
 
+  if (suggest.version == 3 and suggest.section in {ideOutline, ideExpand}):
+    result.add(sep)
+    result.add($suggest.endLine)
+    result.add(sep)
+    result.add($suggest.endCol)
+
 proc suggestResult*(conf: ConfigRef; s: Suggest) =
   if not isNil(conf.suggestionResultHook):
     conf.suggestionResultHook(s)
diff --git a/compiler/vm.nim b/compiler/vm.nim
index 4f5f8485b..424685e40 100644
--- a/compiler/vm.nim
+++ b/compiler/vm.nim
@@ -2333,6 +2333,9 @@ const evalPass* = makePass(myOpen, myProcess, myClose)
 proc evalConstExprAux(module: PSym; idgen: IdGenerator;
                       g: ModuleGraph; prc: PSym, n: PNode,
                       mode: TEvalMode): PNode =
+  when defined(nimsuggest):
+    if g.config.expandDone():
+      return n
   #if g.config.errorCounter > 0: return n
   let n = transformExpr(g, idgen, module, n)
   setupGlobalCtx(module, g, idgen)
diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim
index c5e015ce9..a0a3b8e82 100644
--- a/nimsuggest/nimsuggest.nim
+++ b/nimsuggest/nimsuggest.nim
@@ -27,7 +27,7 @@ import compiler / [options, commands, modules, sem,
   passes, passaux, msgs,
   sigmatch, ast,
   idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper,
-  pathutils, condsyms]
+  pathutils, condsyms, syntaxes]
 
 when defined(nimPreviewSlimSystem):
   import std/typedthreads
@@ -88,7 +88,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) =
@@ -140,6 +140,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()
@@ -175,12 +178,23 @@ 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 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)
+    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 &
@@ -219,7 +233,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 +244,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 +255,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,6 +476,7 @@ 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
@@ -478,6 +496,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 +505,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 +519,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:
@@ -732,17 +742,21 @@ func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair]
       result.add(itm)
   result.reverse()
 
-proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
+proc findSymData(graph: ModuleGraph, trackPos: TLineInfo):
     ref SymInfoPair =
-  let
-    fileIdx = fileInfoIdx(graph.config, file)
-    trackPos = newLineInfo(fileIdx, line, col)
-  for s in graph.fileSymbols(fileIdx).deduplicateSymInfoPair:
+  for s in graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair:
     if isTracked(s.info, trackPos, s.sym.name.s.len):
       new(result)
       result[] = s
       break
 
+proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
+    ref SymInfoPair =
+  let
+    fileIdx = fileInfoIdx(graph.config, file)
+    trackPos = newLineInfo(fileIdx, line, col)
+  result = findSymData(graph, trackPos)
+
 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:
@@ -752,7 +766,8 @@ proc markDirtyIfNeeded(graph: ModuleGraph, file: string, originalFileIdx: FileIn
   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):
@@ -760,7 +775,8 @@ 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)
 
 const
@@ -771,7 +787,75 @@ 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: seq[SymInfoPair]):
+    ref SymInfoPair =
+  for s in infoPairs:
+    if s.info.exactEquals trackPos:
+      new(result)
+      result[] = s
+      break
+
+proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: seq[SymInfoPair]): 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: seq[SymInfoPair]): 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: seq[SymInfoPair]) =
+  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
@@ -783,7 +867,7 @@ 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
 
@@ -811,7 +895,7 @@ 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
+  elif cmd in {ideSug, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration, ideExpand} and
        (graph.needsCompilation(fileIndex) or cmd == ideSug):
     # for ideSug use v2 implementation
     if cmd == ideSug:
@@ -861,16 +945,8 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
     # future calls.
     graph.markDirtyIfNeeded(file.string, fileIndex)
   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:
@@ -933,6 +1009,39 @@ 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
   else:
     myLog fmt "Discarding {cmd}"
 
@@ -1018,9 +1127,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: @[])
diff --git a/nimsuggest/tester.nim b/nimsuggest/tester.nim
index 6e068e067..060335959 100644
--- a/nimsuggest/tester.nim
+++ b/nimsuggest/tester.nim
@@ -66,7 +66,7 @@ proc parseTest(filename: string; epcMode=false): Test =
       elif x.startsWith(">"):
         # since 'markers' here are not complete yet, we do the $substitutions
         # afterwards
-        result.script.add((x.substr(1).replaceWord("$path", tpath), ""))
+        result.script.add((x.substr(1).replaceWord("$path", tpath).replaceWord("$file", filename), ""))
       elif x.len > 0:
         # expected output line:
         let x = x % ["file", filename, "lib", libpath]
@@ -218,7 +218,12 @@ proc sexpToAnswer(s: SexpNode): string =
       result.add doc
       result.add '\t'
       result.addInt a[8].getNum
-      if a.len >= 10:
+      if a.len >= 11:
+        result.add '\t'
+        result.addInt a[9].getNum
+        result.add '\t'
+        result.addInt a[10].getNum
+      elif a.len >= 10:
         result.add '\t'
         result.add a[9].getStr
     result.add '\L'
@@ -229,8 +234,8 @@ proc doReport(filename, answer, resp: string; report: var string) =
     var hasDiff = false
     for i in 0..min(resp.len-1, answer.len-1):
       if resp[i] != answer[i]:
-        report.add "\n  Expected:  " & resp.substr(i, i+200)
-        report.add "\n  But got:   " & answer.substr(i, i+200)
+        report.add "\n  Expected:\n" & resp
+        report.add "\n  But got:\n" & answer
         hasDiff = true
         break
     if not hasDiff:
@@ -342,8 +347,8 @@ proc main() =
   if os.paramCount() > 0:
     let x = os.paramStr(1)
     let xx = expandFilename x
+    # run only stdio when running single test
     failures += runTest(xx)
-    failures += runEpcTest(xx)
   else:
     let files = toSeq(walkFiles(tpath / "t*.nim"))
     for i, x in files:
diff --git a/nimsuggest/tests/tv3.nim b/nimsuggest/tests/tv3.nim
index 9d8b1ef2d..fd736a1d8 100644
--- a/nimsuggest/tests/tv3.nim
+++ b/nimsuggest/tests/tv3.nim
@@ -16,10 +16,6 @@ def	skField	tv3.Foo.bar	string	$file	5	4	""	100
 use	skField	tv3.Foo.bar	string	$file	8	9	""	100
 >def $1
 def	skField	tv3.Foo.bar	string	$file	5	4	""	100
->outline $1
-outline	skType	tv3.Foo	Foo	$file	4	2	""	100
-outline	skField	tv3.Foo.bar	string	$file	5	4	""	100
-outline	skProc	tv3.test	proc (f: Foo){.gcsafe.}	$file	7	5	""	100
 >sug $1
 sug	skField	bar	string	$file	5	4	""	100	Prefix
 >globalSymbols test
diff --git a/nimsuggest/tests/tv3_outline.nim b/nimsuggest/tests/tv3_outline.nim
new file mode 100644
index 000000000..6370948d9
--- /dev/null
+++ b/nimsuggest/tests/tv3_outline.nim
@@ -0,0 +1,45 @@
+# tests v3 outline
+
+type
+  Foo* = ref object of RootObj
+    bar*: string
+  FooEnum = enum value1, value2
+  FooPrivate = ref object of RootObj
+    barPrivate: string
+
+macro m(arg: untyped): untyped = discard
+template t(arg: untyped): untyped = discard
+proc p(): void = discard
+iterator i(): int = discard
+converter c(s: string): int = discard
+method m(f: Foo): void = discard
+func f(): void = discard
+
+let a = 1
+var b = 2
+const con = 2
+
+proc outer(): void =
+  proc inner() = discard
+
+proc procWithLocal(): void =
+  let local = 10
+
+discard """
+$nimsuggest --v3 --tester $file
+>outline $file
+outline	skType	tv3_outline.Foo	Foo	$file	4	2	""	100	5	16
+outline	skType	tv3_outline.FooEnum	FooEnum	$file	6	2	""	100	6	31
+outline	skEnumField	tv3_outline.FooEnum.value1	FooEnum	$file	6	17	""	100	6	23
+outline	skEnumField	tv3_outline.FooEnum.value2	FooEnum	$file	6	25	""	100	6	31
+outline	skType	tv3_outline.FooPrivate	FooPrivate	$file	7	2	""	100	8	22
+outline	skMacro	tv3_outline.m	macro (arg: untyped): untyped{.noSideEffect, gcsafe.}	$file	10	6	""	100	10	40
+outline	skTemplate	tv3_outline.t	template (arg: untyped): untyped	$file	11	9	""	100	11	43
+outline	skProc	tv3_outline.p	proc (){.noSideEffect, gcsafe.}	$file	12	5	""	100	12	24
+outline	skConverter	tv3_outline.c	converter (s: string): int{.noSideEffect, gcsafe.}	$file	14	10	""	100	14	37
+outline	skFunc	tv3_outline.f	proc (){.noSideEffect, gcsafe.}	$file	16	5	""	100	16	24
+outline	skConst	tv3_outline.con	int literal(2)	$file	20	6	""	100	20	13
+outline	skProc	tv3_outline.outer	proc (){.noSideEffect, gcsafe.}	$file	22	5	""	100	23	24
+outline	skProc	tv3_outline.outer.inner	proc (){.noSideEffect, gcsafe.}	$file	23	7	""	100	23	24
+outline	skProc	tv3_outline.procWithLocal	proc (){.noSideEffect, gcsafe.}	$file	25	5	""	100	26	16
+"""