summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/modulegraphs.nim25
-rw-r--r--compiler/options.nim2
-rw-r--r--compiler/sempass2.nim63
-rw-r--r--compiler/suggest.nim44
-rw-r--r--compiler/suggestsymdb.nim212
-rw-r--r--nimsuggest/nimsuggest.nim176
-rw-r--r--nimsuggest/tester.nim8
7 files changed, 480 insertions, 50 deletions
diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim
index 9885a9b45..09eaf5f6d 100644
--- a/compiler/modulegraphs.nim
+++ b/compiler/modulegraphs.nim
@@ -11,9 +11,9 @@
 ## represents a complete Nim project. Single modules can either be kept in RAM
 ## or stored in a rod-file.
 
-import std/[intsets, tables, hashes, strtabs]
+import std/[intsets, tables, hashes, strtabs, algorithm]
 import ../dist/checksums/src/checksums/md5
-import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils, packages
+import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils, packages, suggestsymdb
 import ic / [packed_ast, ic]
 
 
@@ -55,11 +55,6 @@ type
     concreteTypes*: seq[FullId]
     inst*: PInstantiation
 
-  SymInfoPair* = object
-    sym*: PSym
-    info*: TLineInfo
-    isDecl*: bool
-
   PipelinePass* = enum
     NonePass
     SemPass
@@ -108,7 +103,7 @@ type
     doStopCompile*: proc(): bool {.closure.}
     usageSym*: PSym # for nimsuggest
     owners*: seq[PSym]
-    suggestSymbols*: Table[FileIndex, seq[SymInfoPair]]
+    suggestSymbols*: SuggestSymbolDatabase
     suggestErrors*: Table[FileIndex, seq[Suggest]]
     methods*: seq[tuple[methods: seq[PSym], dispatcher: PSym]] # needs serialization!
     bucketTable*: CountTable[ItemId]
@@ -506,7 +501,7 @@ proc initModuleGraphFields(result: ModuleGraph) =
   result.importStack = @[]
   result.inclToMod = initTable[FileIndex, FileIndex]()
   result.owners = @[]
-  result.suggestSymbols = initTable[FileIndex, seq[SymInfoPair]]()
+  result.suggestSymbols = initTable[FileIndex, SuggestFileSymbolDatabase]()
   result.suggestErrors = initTable[FileIndex, seq[Suggest]]()
   result.methods = @[]
   result.compilerprocs = initStrTable()
@@ -712,16 +707,14 @@ func belongsToStdlib*(graph: ModuleGraph, sym: PSym): bool =
   ## Check if symbol belongs to the 'stdlib' package.
   sym.getPackageSymbol.getPackageId == graph.systemModule.getPackageId
 
-proc `==`*(a, b: SymInfoPair): bool =
-  result = a.sym == b.sym and a.info.exactEquals(b.info)
-
-proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): seq[SymInfoPair] =
-  result = graph.suggestSymbols.getOrDefault(fileIdx, @[])
+proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): SuggestFileSymbolDatabase =
+  result = graph.suggestSymbols.getOrDefault(fileIdx, newSuggestFileSymbolDatabase(fileIdx, optIdeExceptionInlayHints in graph.config.globalOptions))
+  doAssert(result.fileIndex == fileIdx)
 
 iterator suggestSymbolsIter*(g: ModuleGraph): SymInfoPair =
   for xs in g.suggestSymbols.values:
-    for x in xs:
-      yield x
+    for i in xs.lineInfo.low..xs.lineInfo.high:
+      yield xs.getSymInfoPair(i)
 
 iterator suggestErrorsIter*(g: ModuleGraph): Suggest =
   for xs in g.suggestErrors.values:
diff --git a/compiler/options.nim b/compiler/options.nim
index 3be1758eb..356aa6cc8 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -86,6 +86,7 @@ type                          # please make sure we have under 32 options
                               # also: generate header file
     optIdeDebug               # idetools: debug mode
     optIdeTerse               # idetools: use terse descriptions
+    optIdeExceptionInlayHints
     optExcessiveStackTrace    # fully qualified module filenames
     optShowAllMismatches      # show all overloading resolution candidates
     optWholeProject           # for 'doc': output any dependency
@@ -298,6 +299,7 @@ type
   SuggestInlayHintKind* = enum
     sihkType = "Type",
     sihkParameter = "Parameter"
+    sihkException = "Exception"
 
   SuggestInlayHint* = ref object
     kind*: SuggestInlayHintKind
diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim
index 7ddd371cd..64f6e42e2 100644
--- a/compiler/sempass2.nim
+++ b/compiler/sempass2.nim
@@ -11,9 +11,9 @@ import
   ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
   wordrecg, options, guards, lineinfos, semfold, semdata,
   modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling,
-  semstrictfuncs
+  semstrictfuncs, suggestsymdb
 
-import std/[tables, intsets, strutils]
+import std/[tables, intsets, strutils, sequtils]
 
 when defined(nimPreviewSlimSystem):
   import std/assertions
@@ -66,8 +66,12 @@ discard """
 """
 
 type
+  CaughtExceptionsStack = object
+    nodes: seq[seq[PType]]
   TEffects = object
     exc: PNode  # stack of exceptions
+    when defined(nimsuggest):
+      caughtExceptions: CaughtExceptionsStack
     tags: PNode # list of tags
     forbids: PNode # list of tags
     bottom, inTryStmt, inExceptOrFinallyStmt, leftPartOfAsgn, inIfStmt, currentBlock: int
@@ -411,7 +415,7 @@ proc throws(tracked, n, orig: PNode) =
     else:
       tracked.add n
 
-proc getEbase(g: ModuleGraph; info: TLineInfo): PType =
+proc getEbase*(g: ModuleGraph; info: TLineInfo): PType =
   result = g.sysTypeFromName(info, "Exception")
 
 proc excType(g: ModuleGraph; n: PNode): PType =
@@ -492,6 +496,18 @@ proc catchesAll(tracked: PEffects) =
   if tracked.exc.len > 0:
     setLen(tracked.exc.sons, tracked.bottom)
 
+proc push(s: var CaughtExceptionsStack) =
+  s.nodes.add(@[])
+
+proc pop(s: var CaughtExceptionsStack) =
+  s.nodes.del(high(s.nodes))
+
+proc addCatch(s: var CaughtExceptionsStack, e: PType) =
+  s.nodes[high(s.nodes)].add(e)
+
+proc addCatchAll(s: var CaughtExceptionsStack) =
+  s.nodes[high(s.nodes)].add(nil)
+
 proc track(tracked: PEffects, n: PNode)
 proc trackTryStmt(tracked: PEffects, n: PNode) =
   let oldBottom = tracked.bottom
@@ -500,12 +516,33 @@ proc trackTryStmt(tracked: PEffects, n: PNode) =
   let oldState = tracked.init.len
   var inter: TIntersection = @[]
 
+  when defined(nimsuggest):
+    tracked.caughtExceptions.push
+    for i in 1..<n.len:
+      let b = n[i]
+      if b.kind == nkExceptBranch:
+        if b.len == 1:
+          tracked.caughtExceptions.addCatchAll
+        else:
+          for j in 0..<b.len - 1:
+            if b[j].isInfixAs():
+              assert(b[j][1].kind == nkType)
+              tracked.caughtExceptions.addCatch(b[j][1].typ)
+            else:
+              assert(b[j].kind == nkType)
+              tracked.caughtExceptions.addCatch(b[j].typ)
+      else:
+        assert b.kind == nkFinally
+
   inc tracked.inTryStmt
   track(tracked, n[0])
   dec tracked.inTryStmt
   for i in oldState..<tracked.init.len:
     addToIntersection(inter, tracked.init[i], bsNone)
 
+  when defined(nimsuggest):
+    tracked.caughtExceptions.pop
+
   var branches = 1
   var hasFinally = false
   inc tracked.inExceptOrFinallyStmt
@@ -917,6 +954,19 @@ proc checkForSink(tracked: PEffects; n: PNode) =
   if tracked.inIfStmt == 0 and optSinkInference in tracked.config.options:
     checkForSink(tracked.config, tracked.c.idgen, tracked.owner, n)
 
+proc markCaughtExceptions(tracked: PEffects; g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym) =
+  when defined(nimsuggest):
+    proc internalMarkCaughtExceptions(tracked: PEffects; q: var SuggestFileSymbolDatabase; info: TLineInfo) =
+      var si = q.findSymInfoIndex(info)
+      if si != -1:
+        q.caughtExceptionsSet[si] = true
+        for w1 in tracked.caughtExceptions.nodes:
+          for w2 in w1:
+            q.caughtExceptions[si].add(w2)
+
+    if optIdeExceptionInlayHints in tracked.config.globalOptions:
+      internalMarkCaughtExceptions(tracked, g.suggestSymbols.mgetOrPut(info.fileIndex, newSuggestFileSymbolDatabase(info.fileIndex, true)), info)
+
 proc trackCall(tracked: PEffects; n: PNode) =
   template gcsafeAndSideeffectCheck() =
     if notGcSafe(op) and not importedFromC(a):
@@ -937,6 +987,13 @@ proc trackCall(tracked: PEffects; n: PNode) =
     if tracked.owner.kind != skMacro and n.typ.skipTypes(abstractVar).kind != tyOpenArray:
       createTypeBoundOps(tracked, n.typ, n.info)
 
+  when defined(nimsuggest):
+    var actualLoc = a.info
+    if n.kind == nkHiddenCallConv:
+      actualLoc = n.info
+    if a.kind == nkSym:
+      markCaughtExceptions(tracked, tracked.graph, actualLoc, a.sym, tracked.graph.usageSym)
+
   let notConstExpr = getConstExpr(tracked.ownerModule, n, tracked.c.idgen, tracked.graph) == nil
   if notConstExpr:
     if a.kind == nkCast and a[1].typ.kind == tyProc:
diff --git a/compiler/suggest.nim b/compiler/suggest.nim
index 21986b4d9..616ecd466 100644
--- a/compiler/suggest.nim
+++ b/compiler/suggest.nim
@@ -32,7 +32,7 @@
 
 # included from sigmatch.nim
 
-import prefixmatches
+import prefixmatches, suggestsymdb
 from wordrecg import wDeprecated, wError, wAddr, wYield
 
 import std/[algorithm, sets, parseutils, tables]
@@ -114,6 +114,10 @@ proc getTokenLenFromSource(conf: ConfigRef; ident: string; info: TLineInfo; skip
     result = skipUntil(line, '`', column)
     if cmpIgnoreStyle(line[column..column + result - 1], ident) != 0:
       result = 0
+  elif column >= 0 and line[column] == '`' and isOpeningBacktick(column):
+    result = skipUntil(line, '`', column + 1) + 2
+    if cmpIgnoreStyle(line[column + 1..column + result - 2], ident) != 0:
+      result = 0
   elif ident[0] in linter.Letters and ident[^1] != '=':
     result = identLen(line, column)
     if cmpIgnoreStyle(line[column..column + result - 1], ident[0..min(result-1,len(ident)-1)]) != 0:
@@ -265,7 +269,7 @@ proc `$`*(suggest: Suggest): string =
       result.add(sep)
       result.add($suggest.endCol)
 
-proc suggestToSuggestInlayHint*(sug: Suggest): SuggestInlayHint =
+proc suggestToSuggestInlayTypeHint*(sug: Suggest): SuggestInlayHint =
   SuggestInlayHint(
     kind: sihkType,
     line: sug.line,
@@ -277,6 +281,30 @@ proc suggestToSuggestInlayHint*(sug: Suggest): SuggestInlayHint =
     tooltip: ""
   )
 
+proc suggestToSuggestInlayExceptionHintLeft*(sug: Suggest, propagatedExceptions: seq[PType]): SuggestInlayHint =
+  SuggestInlayHint(
+    kind: sihkException,
+    line: sug.line,
+    column: sug.column,
+    label: "try ",
+    paddingLeft: false,
+    paddingRight: false,
+    allowInsert: false,
+    tooltip: "propagated exceptions: " & $propagatedExceptions
+  )
+
+proc suggestToSuggestInlayExceptionHintRight*(sug: Suggest, propagatedExceptions: seq[PType]): SuggestInlayHint =
+  SuggestInlayHint(
+    kind: sihkException,
+    line: sug.line,
+    column: sug.column + sug.tokenLen,
+    label: "!",
+    paddingLeft: false,
+    paddingRight: false,
+    allowInsert: false,
+    tooltip: "propagated exceptions: " & $propagatedExceptions
+  )
+
 proc suggestResult*(conf: ConfigRef; s: Suggest) =
   if not isNil(conf.suggestionResultHook):
     conf.suggestionResultHook(s)
@@ -534,6 +562,16 @@ proc isTracked*(current, trackPos: TLineInfo, tokenLen: int): bool =
   else:
     result = false
 
+proc isTracked*(current, trackPos: TinyLineInfo, tokenLen: int): bool =
+  if current.line==trackPos.line:
+    let col = trackPos.col
+    if col >= current.col and col <= current.col+tokenLen-1:
+      result = true
+    else:
+      result = false
+  else:
+    result = false
+
 when defined(nimsuggest):
   # Since TLineInfo defined a == operator that doesn't include the column,
   # we map TLineInfo to a unique int here for this lookup table:
@@ -584,7 +622,7 @@ proc suggestSym*(g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym; i
   ## misnamed: should be 'symDeclared'
   let conf = g.config
   when defined(nimsuggest):
-    g.suggestSymbols.mgetOrPut(info.fileIndex, @[]).add SymInfoPair(sym: s, info: info, isDecl: isDecl)
+    g.suggestSymbols.add SymInfoPair(sym: s, info: info, isDecl: isDecl), optIdeExceptionInlayHints in g.config.globalOptions
 
     if conf.suggestVersion == 0:
       if s.allUsages.len == 0:
diff --git a/compiler/suggestsymdb.nim b/compiler/suggestsymdb.nim
new file mode 100644
index 000000000..e1e67afbe
--- /dev/null
+++ b/compiler/suggestsymdb.nim
@@ -0,0 +1,212 @@
+import std/[intsets, tables, algorithm, assertions]
+import ast, lineinfos, msgs
+
+type
+  PackedBoolArray* = object
+    s: IntSet
+    len: int
+
+  TinyLineInfo* = object
+    line*: uint16
+    col*: int16
+
+  SymInfoPair* = object
+    sym*: PSym
+    info*: TLineInfo
+    caughtExceptions*: seq[PType]
+    caughtExceptionsSet*: bool
+    isDecl*: bool
+
+  SuggestFileSymbolDatabase* = object
+    lineInfo*: seq[TinyLineInfo]
+    sym*: seq[PSym]
+    caughtExceptions*: seq[seq[PType]]
+    caughtExceptionsSet*: PackedBoolArray
+    isDecl*: PackedBoolArray
+    fileIndex*: FileIndex
+    trackCaughtExceptions*: bool
+    isSorted*: bool
+
+  SuggestSymbolDatabase* = Table[FileIndex, SuggestFileSymbolDatabase]
+
+
+func newPackedBoolArray*(): PackedBoolArray =
+  PackedBoolArray(
+    s: initIntSet(),
+    len: 0
+  )
+
+func low*(s: PackedBoolArray): int =
+  0
+
+func high*(s: PackedBoolArray): int =
+  s.len - 1
+
+func `[]`*(s: PackedBoolArray; idx: int): bool =
+  s.s.contains(idx)
+
+proc `[]=`*(s: var PackedBoolArray; idx: int; v: bool) =
+  if v:
+    s.s.incl(idx)
+  else:
+    s.s.excl(idx)
+
+proc add*(s: var PackedBoolArray; v: bool) =
+  inc(s.len)
+  if v:
+    s.s.incl(s.len - 1)
+
+proc reverse*(s: var PackedBoolArray) =
+  var
+    reversedSet = initIntSet()
+  for i in 0..s.high:
+    if s.s.contains(i):
+      reversedSet.incl(s.high - i)
+  s.s = reversedSet
+
+proc getSymInfoPair*(s: SuggestFileSymbolDatabase; idx: int): SymInfoPair =
+  SymInfoPair(
+    sym: s.sym[idx],
+    info: TLineInfo(
+      line: s.lineInfo[idx].line,
+      col: s.lineInfo[idx].col,
+      fileIndex: s.fileIndex
+    ),
+    caughtExceptions:
+      if s.trackCaughtExceptions:
+        s.caughtExceptions[idx]
+      else:
+        @[],
+    caughtExceptionsSet:
+      if s.trackCaughtExceptions:
+        s.caughtExceptionsSet[idx]
+      else:
+        false,
+    isDecl: s.isDecl[idx]
+  )
+
+proc reverse*(s: var SuggestFileSymbolDatabase) =
+  s.lineInfo.reverse()
+  s.sym.reverse()
+  s.caughtExceptions.reverse()
+  s.caughtExceptionsSet.reverse()
+  s.isDecl.reverse()
+
+proc newSuggestFileSymbolDatabase*(aFileIndex: FileIndex; aTrackCaughtExceptions: bool): SuggestFileSymbolDatabase =
+  SuggestFileSymbolDatabase(
+    lineInfo: @[],
+    sym: @[],
+    caughtExceptions: @[],
+    caughtExceptionsSet: newPackedBoolArray(),
+    isDecl: newPackedBoolArray(),
+    fileIndex: aFileIndex,
+    trackCaughtExceptions: aTrackCaughtExceptions,
+    isSorted: true
+  )
+
+proc exactEquals*(a, b: TinyLineInfo): bool =
+  result = a.line == b.line and a.col == b.col
+
+proc `==`*(a, b: SymInfoPair): bool =
+  result = a.sym == b.sym and a.info.exactEquals(b.info)
+
+func cmp*(a: TinyLineInfo; b: TinyLineInfo): int =
+  result = cmp(a.line, b.line)
+  if result == 0:
+    result = cmp(a.col, b.col)
+
+func compare*(s: var SuggestFileSymbolDatabase; i, j: int): int =
+  result = cmp(s.lineInfo[i], s.lineInfo[j])
+  if result == 0:
+    result = cmp(s.isDecl[i], s.isDecl[j])
+
+proc exchange(s: var SuggestFileSymbolDatabase; i, j: int) =
+  if i == j:
+    return
+  var tmp1 = s.lineInfo[i]
+  s.lineInfo[i] = s.lineInfo[j]
+  s.lineInfo[j] = tmp1
+  if s.trackCaughtExceptions:
+    var tmp2 = s.caughtExceptions[i]
+    s.caughtExceptions[i] = s.caughtExceptions[j]
+    s.caughtExceptions[j] = tmp2
+    var tmp3 = s.caughtExceptionsSet[i]
+    s.caughtExceptionsSet[i] = s.caughtExceptionsSet[j]
+    s.caughtExceptionsSet[j] = tmp3
+  var tmp4 = s.isDecl[i]
+  s.isDecl[i] = s.isDecl[j]
+  s.isDecl[j] = tmp4
+  var tmp5 = s.sym[i]
+  s.sym[i] = s.sym[j]
+  s.sym[j] = tmp5
+
+proc quickSort(s: var SuggestFileSymbolDatabase; ll, rr: int) =
+  var
+    i, j, pivotIdx: int
+    l = ll
+    r = rr
+  while true:
+    i = l
+    j = r
+    pivotIdx = l + ((r - l) shr 1)
+    while true:
+      while (i < pivotIdx) and (s.compare(pivotIdx, i) > 0):
+        inc i
+      while (j > pivotIdx) and (s.compare(pivotIdx, j) < 0):
+        dec j
+      if i < j:
+        s.exchange(i, j)
+        if pivotIdx == i:
+          pivotIdx = j
+          inc i
+        elif pivotIdx == j:
+          pivotIdx = i
+          dec j
+        else:
+          inc i
+          dec j
+      else:
+        break
+    if (pivotIdx - l) < (r - pivotIdx):
+      if (l + 1) < pivotIdx:
+        s.quickSort(l, pivotIdx - 1)
+      l = pivotIdx + 1
+    else:
+      if (pivotIdx + 1) < r:
+        s.quickSort(pivotIdx + 1, r)
+      if (l + 1) < pivotIdx:
+        r = pivotIdx - 1
+      else:
+        break
+    if l >= r:
+      break
+
+proc sort*(s: var SuggestFileSymbolDatabase) =
+  s.quickSort(s.lineInfo.low, s.lineInfo.high)
+  s.isSorted = true
+
+proc add*(s: var SuggestFileSymbolDatabase; v: SymInfoPair) =
+  doAssert(v.info.fileIndex == s.fileIndex)
+  s.lineInfo.add(TinyLineInfo(
+    line: v.info.line,
+    col: v.info.col
+  ))
+  s.sym.add(v.sym)
+  s.isDecl.add(v.isDecl)
+  if s.trackCaughtExceptions:
+    s.caughtExceptions.add(v.caughtExceptions)
+    s.caughtExceptionsSet.add(v.caughtExceptionsSet)
+  s.isSorted = false
+
+proc add*(s: var SuggestSymbolDatabase; v: SymInfoPair; trackCaughtExceptions: bool) =
+  s.mgetOrPut(v.info.fileIndex, newSuggestFileSymbolDatabase(v.info.fileIndex, trackCaughtExceptions)).add(v)
+
+proc findSymInfoIndex*(s: var SuggestFileSymbolDatabase; li: TLineInfo): int =
+  doAssert(li.fileIndex == s.fileIndex)
+  if not s.isSorted:
+    s.sort()
+  var q = TinyLineInfo(
+    line: li.line,
+    col: li.col
+  )
+  result = binarySearch(s.lineInfo, q, cmp)
diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim
index e1bb0d5aa..67fc2f8c4 100644
--- a/nimsuggest/nimsuggest.nim
+++ b/nimsuggest/nimsuggest.nim
@@ -8,6 +8,10 @@
 #
 
 import compiler/renderer
+import compiler/types
+import compiler/trees
+import compiler/wordrecg
+import compiler/sempass2
 import strformat
 import algorithm
 import tables
@@ -34,7 +38,7 @@ import compiler / [options, commands, modules,
   passes, passaux, msgs,
   sigmatch, ast,
   idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper,
-  pathutils, condsyms, syntaxes]
+  pathutils, condsyms, syntaxes, suggestsymdb]
 
 when defined(nimPreviewSlimSystem):
   import std/typedthreads
@@ -74,6 +78,8 @@ Options:
   --tester                implies --stdin and outputs a line
                           '""" & DummyEof & """' for the tester
   --find                  attempts to find the project file of the current project
+  --exceptionInlayHints:on|off
+                          globally turn exception inlay hints on|off
 
 The server then listens to the connection and takes line-based commands.
 
@@ -127,7 +133,8 @@ const
          "type 'terse' to toggle terse mode on/off"
   #List of currently supported capabilities. So lang servers/ides can iterate over and check for what's enabled
   Capabilities = [
-    "con" #current NimSuggest supports the `con` commmand
+    "con", #current NimSuggest supports the `con` commmand
+    "exceptionInlayHints"
   ]
 
 proc parseQuoted(cmd: string; outp: var string; start: int): int =
@@ -699,6 +706,11 @@ proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =
           quit 0
         else:
           processSwitch(pass, p, conf)
+      of "exceptioninlayhints":
+        case p.val.normalize
+        of "", "on": incl(conf.globalOptions, optIdeExceptionInlayHints)
+        of "off": excl(conf.globalOptions, optIdeExceptionInlayHints)
+        else: processSwitch(pass, p, conf)
       of "tester":
         gMode = mstdin
         gEmitEof = true
@@ -802,25 +814,57 @@ func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair]
       result.add(itm)
   result.reverse()
 
+func deduplicateSymInfoPair(xs: SuggestFileSymbolDatabase): SuggestFileSymbolDatabase =
+  # xs contains duplicate items and we want to filter them by range because the
+  # sym may not match. This can happen when xs contains the same definition but
+  # with different signature because suggestSym might be called multiple times
+  # for the same symbol (e. g. including/excluding the pragma)
+  result = SuggestFileSymbolDatabase(
+    lineInfo: newSeqOfCap[TinyLineInfo](xs.lineInfo.len),
+    sym: newSeqOfCap[PSym](xs.sym.len),
+    isDecl: newPackedBoolArray(),
+    caughtExceptions: newSeqOfCap[seq[PType]](xs.caughtExceptions.len),
+    caughtExceptionsSet: newPackedBoolArray(),
+    fileIndex: xs.fileIndex,
+    trackCaughtExceptions: xs.trackCaughtExceptions,
+    isSorted: false
+  )
+  var i = xs.lineInfo.high
+  while i >= 0:
+    let itm = xs.lineInfo[i]
+    var found = false
+    for res in result.lineInfo:
+      if res.exactEquals(itm):
+        found = true
+        break
+    if not found:
+      result.add(xs.getSymInfoPair(i))
+    dec i
+  result.reverse()
+
 proc findSymData(graph: ModuleGraph, trackPos: TLineInfo):
     ref SymInfoPair =
-  for s in graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair:
-    if isTracked(s.info, trackPos, s.sym.name.s.len):
+  let db = graph.fileSymbols(trackPos.fileIndex).deduplicateSymInfoPair
+  doAssert(db.fileIndex == trackPos.fileIndex)
+  for i in db.lineInfo.low..db.lineInfo.high:
+    if isTracked(db.lineInfo[i], TinyLineInfo(line: trackPos.line, col: trackPos.col), db.sym[i].name.s.len):
+      var res = db.getSymInfoPair(i)
       new(result)
-      result[] = s
+      result[] = res
       break
 
-func isInRange*(current, startPos, endPos: TLineInfo, tokenLen: int): bool =
-  result = current.fileIndex == startPos.fileIndex and
+func isInRange*(current, startPos, endPos: TinyLineInfo, tokenLen: int): bool =
+  result =
     (current.line > startPos.line or (current.line == startPos.line and current.col>=startPos.col)) and
     (current.line < endPos.line or (current.line == endPos.line and current.col <= endPos.col))
 
 proc findSymDataInRange(graph: ModuleGraph, startPos, endPos: TLineInfo):
     seq[SymInfoPair] =
   result = newSeq[SymInfoPair]()
-  for s in graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair:
-    if isInRange(s.info, startPos, endPos, s.sym.name.s.len):
-      result.add(s)
+  let db = graph.fileSymbols(startPos.fileIndex).deduplicateSymInfoPair
+  for i in db.lineInfo.low..db.lineInfo.high:
+    if isInRange(db.lineInfo[i], TinyLineInfo(line: startPos.line, col: startPos.col), TinyLineInfo(line: endPos.line, col: endPos.col), db.sym[i].name.s.len):
+      result.add(db.getSymInfoPair(i))
 
 proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
     ref SymInfoPair =
@@ -859,7 +903,7 @@ proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
                              endLine = endLine, endCol = endCol)
   suggestResult(graph.config, suggest)
 
-proc suggestInlayHintResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
+proc suggestInlayHintResultType(graph: ModuleGraph, sym: PSym, info: TLineInfo,
                    defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
   let section = if defaultSection != ideNone:
                   defaultSection
@@ -870,12 +914,61 @@ proc suggestInlayHintResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
   var suggestDef = symToSuggest(graph, sym, isLocal=false, section,
                                 info, 100, PrefixMatch.None, false, 0, true,
                                 endLine = endLine, endCol = endCol)
-  suggestDef.inlayHintInfo = suggestToSuggestInlayHint(suggestDef)
+  suggestDef.inlayHintInfo = suggestToSuggestInlayTypeHint(suggestDef)
   suggestDef.section = ideInlayHints
   if sym.kind == skForVar:
     suggestDef.inlayHintInfo.allowInsert = false
   suggestResult(graph.config, suggestDef)
 
+proc suggestInlayHintResultException(graph: ModuleGraph, sym: PSym, info: TLineInfo,
+                   defaultSection = ideNone, caughtExceptions: seq[PType], caughtExceptionsSet: bool, endLine: uint16 = 0, endCol = 0) =
+  if not caughtExceptionsSet:
+    return
+
+  if sym.kind == skParam and sfEffectsDelayed in sym.flags:
+    return
+
+  var raisesList: seq[PType] = @[getEbase(graph, info)]
+
+  let t = sym.typ
+  if not isNil(t) and not isNil(t.n) and t.n.len > 0 and t.n[0].len > exceptionEffects:
+    let effects = t.n[0]
+    if effects.kind == nkEffectList and effects.len == effectListLen:
+      let effs = effects[exceptionEffects]
+      if not isNil(effs):
+        raisesList = @[]
+        for eff in items(effs):
+          if not isNil(eff):
+            raisesList.add(eff.typ)
+
+  var propagatedExceptionList: seq[PType] = @[]
+  for re in raisesList:
+    var exceptionIsPropagated = true
+    for ce in caughtExceptions:
+      if isNil(ce) or safeInheritanceDiff(re, ce) <= 0:
+        exceptionIsPropagated = false
+        break
+    if exceptionIsPropagated:
+      propagatedExceptionList.add(re)
+
+  if propagatedExceptionList.len == 0:
+    return
+
+  let section = if defaultSection != ideNone:
+                  defaultSection
+                elif sym.info.exactEquals(info):
+                  ideDef
+                else:
+                  ideUse
+  var suggestDef = symToSuggest(graph, sym, isLocal=false, section,
+                                info, 100, PrefixMatch.None, false, 0, true,
+                                endLine = endLine, endCol = endCol)
+  suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintLeft(suggestDef, propagatedExceptionList)
+  suggestDef.section = ideInlayHints
+  suggestResult(graph.config, suggestDef)
+  suggestDef.inlayHintInfo = suggestToSuggestInlayExceptionHintRight(suggestDef, propagatedExceptionList)
+  suggestResult(graph.config, suggestDef)
+
 const
   # kinds for ideOutline and ideGlobalSymbols
   searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate}
@@ -893,15 +986,18 @@ proc findDef(n: PNode, line: uint16, col: int16): PNode =
       let res = findDef(n[i], line, col)
       if res != nil: return res
 
-proc findByTLineInfo(trackPos: TLineInfo, infoPairs: seq[SymInfoPair]):
+proc findByTLineInfo(trackPos: TLineInfo, infoPairs: SuggestFileSymbolDatabase):
     ref SymInfoPair =
-  for s in infoPairs:
-    if s.info.exactEquals trackPos:
-      new(result)
-      result[] = s
-      break
+  result = nil
+  if infoPairs.fileIndex == trackPos.fileIndex:
+    for i in infoPairs.lineInfo.low..infoPairs.lineInfo.high:
+      let s = infoPairs.getSymInfoPair(i)
+      if s.info.exactEquals trackPos:
+        new(result)
+        result[] = s
+        break
 
-proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: seq[SymInfoPair]): bool =
+proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool =
   proc checkSymbol(sym: PSym, info: TLineInfo): bool =
     result = (sym.owner.kind in {skModule, skType} or sym.kind in {skProc, skMethod, skIterator, skTemplate, skType})
 
@@ -915,7 +1011,7 @@ proc outlineNode(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: se
        graph.suggestResult(sym, sym.info, ideOutline, endInfo.line, endInfo.col)
        return true
 
-proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: seq[SymInfoPair]): bool =
+proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPairs: SuggestFileSymbolDatabase): bool =
   for child in n:
     if child.kind in {nkIdent, nkSym}:
       if graph.outlineNode(child, endInfo, infoPairs):
@@ -924,7 +1020,7 @@ proc handleIdentOrSym(graph: ModuleGraph, n: PNode, endInfo: TLineInfo, infoPair
       if graph.handleIdentOrSym(child, endInfo, infoPairs):
         return true
 
-proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: seq[SymInfoPair]) =
+proc iterateOutlineNodes(graph: ModuleGraph, n: PNode, infoPairs: SuggestFileSymbolDatabase) =
   var matched = true
   if n.kind == nkIdent:
     let symData = findByTLineInfo(n.info, infoPairs)
@@ -1028,7 +1124,11 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
   of ideHighlight:
     let sym = graph.findSymData(file, line, col)
     if not sym.isNil:
-      let usages = graph.fileSymbols(fileIndex).filterIt(it.sym == sym.sym)
+      let fs = graph.fileSymbols(fileIndex)
+      var usages: seq[SymInfoPair] = @[]
+      for i in fs.lineInfo.low..fs.lineInfo.high:
+        if fs.sym[i] == sym.sym:
+          usages.add(fs.getSymInfoPair(i))
       myLog fmt "Found {usages.len} usages in {file.string}"
       for s in usages:
         graph.suggestResult(s.sym, s.info)
@@ -1097,9 +1197,10 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
       # find first mention of the symbol in the file containing the definition.
       # It is either the definition or the declaration.
       var first: SymInfoPair
-      for symbol in graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair:
-        if s.sym.symbolEqual(symbol.sym):
-          first = symbol
+      let db = graph.fileSymbols(s.sym.info.fileIndex).deduplicateSymInfoPair
+      for i in db.lineInfo.low..db.lineInfo.high:
+        if s.sym.symbolEqual(db.sym[i]):
+          first = db.getSymInfoPair(i)
           break
 
       if s.info.exactEquals(first.info):
@@ -1150,10 +1251,31 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
     i += parseInt(tag, endLine, i)
     i += skipWhile(tag, seps, i)
     i += parseInt(tag, endCol, i)
+    i += skipWhile(tag, seps, i)
+    var typeHints = true
+    var exceptionHints = false
+    while i <= tag.high:
+      var token: string
+      i += parseUntil(tag, token, seps, i)
+      i += skipWhile(tag, seps, i)
+      case token:
+      of "+typeHints":
+        typeHints = true
+      of "-typeHints":
+        typeHints = false
+      of "+exceptionHints":
+        exceptionHints = true
+      of "-exceptionHints":
+        exceptionHints = false
+      else:
+        myLog fmt "Discarding unknown inlay hint parameter {token}"
+
     let s = graph.findSymDataInRange(file, line, col, endLine, endCol)
     for q in s:
-      if q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType:
-        graph.suggestInlayHintResult(q.sym, q.info, ideInlayHints)
+      if typeHints and q.sym.kind in {skLet, skVar, skForVar, skConst} and q.isDecl and not q.sym.hasUserSpecifiedType:
+        graph.suggestInlayHintResultType(q.sym, q.info, ideInlayHints)
+      if exceptionHints and q.sym.kind in {skProc, skFunc, skMethod, skVar, skLet, skParam} and not q.isDecl:
+        graph.suggestInlayHintResultException(q.sym, q.info, ideInlayHints, caughtExceptions = q.caughtExceptions, caughtExceptionsSet = q.caughtExceptionsSet)
   else:
     myLog fmt "Discarding {cmd}"
 
diff --git a/nimsuggest/tester.nim b/nimsuggest/tester.nim
index 060335959..9b9488348 100644
--- a/nimsuggest/tester.nim
+++ b/nimsuggest/tester.nim
@@ -279,7 +279,13 @@ proc runEpcTest(filename: string): int =
         os.sleep(50)
         inc i
       let a = outp.readAll().strip()
-    let port = parseInt(a)
+    var port: int
+    try:
+      port = parseInt(a)
+    except ValueError:
+      echo "Error parsing port number: " & a
+      echo outp.readAll()
+      quit 1
     socket.connect("localhost", Port(port))
 
     for req, resp in items(s.script):