Fix/improve handling of forward declarations in nimsuggest (#20493)

* Fix/improve handling of forward declarations in nimsuggest

- ideUse now works fine when invoked on the implementation
- implemented ideDeclaration to make cover lsp feature textDocument/declaration
- fixed performance issue related to deduplicating symbols. Now the
deduplication happens after the symbols are filtered. As a alternative we might
change the way cached symbols are stored(e. g. use set).
- I also fixed the way globalSymbols work. Now it will sort the responses based
on the match location to make sure that the results are sorted in user friendly way.

* Update nimsuggest/nimsuggest.nim

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
This commit is contained in:
Ivan Yonchovski 2022-10-06 08:18:46 +03:00 • committed by GitHub
commit 7caa037936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 119 additions and 13 deletions

View file

@ -643,11 +643,11 @@ proc `==`*(a, b: SymInfoPair): bool =
result = a.sym == b.sym and a.info.exactEquals(b.info) result = a.sym == b.sym and a.info.exactEquals(b.info)
proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): seq[SymInfoPair] = proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): seq[SymInfoPair] =
result = graph.suggestSymbols.getOrDefault(fileIdx, @[]).deduplicate result = graph.suggestSymbols.getOrDefault(fileIdx, @[])
iterator suggestSymbolsIter*(g: ModuleGraph): SymInfoPair = iterator suggestSymbolsIter*(g: ModuleGraph): SymInfoPair =
for xs in g.suggestSymbols.values: for xs in g.suggestSymbols.values:
for x in xs.deduplicate: for x in xs:
yield x yield x
iterator suggestErrorsIter*(g: ModuleGraph): Suggest = iterator suggestErrorsIter*(g: ModuleGraph): Suggest =

View file

@ -192,7 +192,7 @@ type
IdeCmd* = enum IdeCmd* = enum
ideNone, ideSug, ideCon, ideDef, ideUse, ideDus, ideChk, ideChkFile, ideMod, ideNone, ideSug, ideCon, ideDef, ideUse, ideDus, ideChk, ideChkFile, ideMod,
ideHighlight, ideOutline, ideKnown, ideMsg, ideProject, ideGlobalSymbols, ideHighlight, ideOutline, ideKnown, ideMsg, ideProject, ideGlobalSymbols,
ideRecompile, ideChanged, ideType ideRecompile, ideChanged, ideType, ideDeclaration
Feature* = enum ## experimental features; DO NOT RENAME THESE! Feature* = enum ## experimental features; DO NOT RENAME THESE!
implicitDeref, implicitDeref,
@ -1026,6 +1026,7 @@ proc `$`*(c: IdeCmd): string =
of ideMsg: "msg" of ideMsg: "msg"
of ideProject: "project" of ideProject: "project"
of ideGlobalSymbols: "globalSymbols" of ideGlobalSymbols: "globalSymbols"
of ideDeclaration: "declaration"
of ideRecompile: "recompile" of ideRecompile: "recompile"
of ideChanged: "changed" of ideChanged: "changed"
of ideType: "type" of ideType: "type"

View file

@ -164,7 +164,7 @@ proc symToSuggest*(g: ModuleGraph; s: PSym, isLocal: bool, section: IdeCmd, info
result.tokenLen = 0 result.tokenLen = 0
else: else:
let infox = let infox =
if useSuppliedInfo or section in {ideUse, ideHighlight, ideOutline}: if useSuppliedInfo or section in {ideUse, ideHighlight, ideOutline, ideDeclaration}:
info info
else: else:
s.info s.info

View file

@ -9,6 +9,7 @@
import compiler/renderer import compiler/renderer
import strformat import strformat
import algorithm
import tables import tables
import std/sha1 import std/sha1
import times import times
@ -149,7 +150,7 @@ proc listEpc(): SexpNode =
argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol)) argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol))
docstring = sexp("line starts at 1, column at 0, dirtyfile is optional") docstring = sexp("line starts at 1, column at 0, dirtyfile is optional")
result = newSList() result = newSList()
for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile"]: for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration"]:
let let
cmd = sexp(command) cmd = sexp(command)
methodDesc = newSList() methodDesc = newSList()
@ -455,6 +456,7 @@ proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) =
of "project": conf.ideCmd = ideProject of "project": conf.ideCmd = ideProject
of "changed": conf.ideCmd = ideChanged of "changed": conf.ideCmd = ideChanged
of "globalsymbols": conf.ideCmd = ideGlobalSymbols of "globalsymbols": conf.ideCmd = ideGlobalSymbols
of "declaration": conf.ideCmd = ideDeclaration
of "chkfile": conf.ideCmd = ideChkFile of "chkfile": conf.ideCmd = ideChkFile
of "recompile": conf.ideCmd = ideRecompile of "recompile": conf.ideCmd = ideRecompile
of "type": conf.ideCmd = ideType of "type": conf.ideCmd = ideType
@ -711,12 +713,28 @@ proc recompilePartially(graph: ModuleGraph, projectFileIdx = InvalidFileIdx) =
except Exception as e: except Exception as e:
myLog fmt "Failed clean recompilation:\n {e.msg} \n\n {e.getStackTrace()}" myLog fmt "Failed clean recompilation:\n {e.msg} \n\n {e.getStackTrace()}"
func deduplicateSymInfoPair[SymInfoPair](xs: seq[SymInfoPair]): seq[SymInfoPair] =
# 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 becase suggestSym might be called multiple times
# for the same symbol (e. g. including/excluding the pragma)
result = @[]
for itm in xs.reversed:
var found = false
for res in result:
if res.info.exactEquals(itm.info):
found = true
break
if not found:
result.add(itm)
result.reverse()
proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int): proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
ref SymInfoPair = ref SymInfoPair =
let let
fileIdx = fileInfoIdx(graph.config, file) fileIdx = fileInfoIdx(graph.config, file)
trackPos = newLineInfo(fileIdx, line, col) trackPos = newLineInfo(fileIdx, line, col)
for s in graph.fileSymbols(fileIdx): for s in graph.fileSymbols(fileIdx).deduplicateSymInfoPair:
if isTracked(s.info, trackPos, s.sym.name.s.len): if isTracked(s.info, trackPos, s.sym.name.s.len):
new(result) new(result)
result[] = s result[] = s
@ -746,6 +764,10 @@ const
# kinds for ideOutline and ideGlobalSymbols # kinds for ideOutline and ideGlobalSymbols
searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate} searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate}
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 executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile, line, col: int;
graph: ModuleGraph) = graph: ModuleGraph) =
let conf = graph.config let conf = graph.config
@ -786,7 +808,7 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
graph.unmarkAllDirty() graph.unmarkAllDirty()
# these commands require partially compiled project # these commands require partially compiled project
elif cmd in {ideSug, ideOutline, ideHighlight, ideDef, ideChkFile, ideType} and elif cmd in {ideSug, ideOutline, ideHighlight, ideDef, ideChkFile, ideType, ideDeclaration} and
(graph.needsCompilation(fileIndex) or cmd == ideSug): (graph.needsCompilation(fileIndex) or cmd == ideSug):
# for ideSug use v2 implementation # for ideSug use v2 implementation
if cmd == ideSug: if cmd == ideSug:
@ -814,8 +836,11 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
of ideUse, ideDus: of ideUse, ideDus:
let symbol = graph.findSymData(file, line, col) let symbol = graph.findSymData(file, line, col)
if not symbol.isNil: if not symbol.isNil:
var res: seq[SymInfoPair] = @[]
for s in graph.suggestSymbolsIter: for s in graph.suggestSymbolsIter:
if s.sym == symbol.sym: if s.sym.symbolEqual(symbol.sym):
res.add(s)
for s in res.deduplicateSymInfoPair():
graph.suggestResult(s.sym, s.info) graph.suggestResult(s.sym, s.info)
of ideHighlight: of ideHighlight:
let sym = graph.findSymData(file, line, col) let sym = graph.findSymData(file, line, col)
@ -836,6 +861,7 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
let let
module = graph.getModule fileIndex module = graph.getModule fileIndex
symbols = graph.fileSymbols(fileIndex) symbols = graph.fileSymbols(fileIndex)
.deduplicateSymInfoPair
.filterIt(it.sym.info.exactEquals(it.info) and .filterIt(it.sym.info.exactEquals(it.info) and
(it.sym.owner == module or (it.sym.owner == module or
it.sym.kind in searchableSymKinds)) it.sym.kind in searchableSymKinds))
@ -852,16 +878,58 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
for error in errors: for error in errors:
suggestResult(graph.config, error) suggestResult(graph.config, error)
of ideGlobalSymbols: of ideGlobalSymbols:
var counter = 0 var
counter = 0
res: seq[SymInfoPair] = @[]
for s in graph.suggestSymbolsIter: for s in graph.suggestSymbolsIter:
if (sfGlobal in s.sym.flags or s.sym.kind in searchableSymKinds) and if (sfGlobal in s.sym.flags or s.sym.kind in searchableSymKinds) and
s.sym.info == s.info: s.sym.info == s.info:
if contains(s.sym.name.s, file.string): if contains(s.sym.name.s, file.string):
inc counter inc counter
graph.suggestResult(s.sym, s.info) res = res.filterIt(not it.info.exactEquals(s.info))
# stop after first 100 results res.add s
if counter > 100: # stop after first 1000 matches...
if counter > 1000:
break break
# ... then sort them by weight ...
res.sort() do (left, right: SymInfoPair) -> int:
let
leftString = left.sym.name.s
rightString = right.sym.name.s
leftIndex = leftString.find(file.string)
rightIndex = rightString.find(file.string)
if leftIndex == rightIndex:
result = cmp(toLowerAscii(leftString),
toLowerAscii(rightString))
else:
result = cmp(leftIndex, rightIndex)
# ... and send first 100 results
if res.len > 0:
for i in 0 .. min(100, res.len - 1):
let s = res[i]
graph.suggestResult(s.sym, s.info)
of ideDeclaration:
let s = graph.findSymData(file, line, col)
if not s.isNil:
# 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
break
if s.info.exactEquals(first.info):
# we are on declaration, go to definition
graph.suggestResult(first.sym, first.sym.info, ideDeclaration)
else:
# we are on definition or usage, look for declaration
graph.suggestResult(first.sym, first.info, ideDeclaration)
else: else:
myLog fmt "Discarding {cmd}" myLog fmt "Discarding {cmd}"

View file

@ -0,0 +1,23 @@
proc de#[!]#mo(): int
proc de#[!]#mo(): int = 5
let a = de#[!]#mo()
discard """
$nimsuggest --v3 --tester $file
>use $1
use skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 1 5 "" 100
def skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 3 5 "" 100
use skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 5 8 "" 100
>use $2
use skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 1 5 "" 100
def skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 3 5 "" 100
use skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 5 8 "" 100
>declaration $1
declaration skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 3 5 "" 100
>declaration $2
declaration skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 1 5 "" 100
>declaration $3
declaration skProc tv3_forward_definition.demo proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 1 5 "" 100
"""

View file

@ -0,0 +1,14 @@
# Tests the order of the matches
proc Btoken(): int = 5
proc tokenA(): int = 5
proc token(): int = 5
proc BBtokenA(): int = 5
discard """
$nimsuggest --v3 --tester $file
>globalSymbols token
def skProc tv3_globalSymbols.token proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 4 5 "" 100
def skProc tv3_globalSymbols.tokenA proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 3 5 "" 100
def skProc tv3_globalSymbols.Btoken proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 2 5 "" 100
def skProc tv3_globalSymbols.BBtokenA proc (): int{.noSideEffect, gcsafe, locks: 0.} $file 5 5 "" 100
"""