Inlay hints support (#22896)

This adds inlay hints support to nimsuggest. It adds a new command to
nimsuggest, called 'inlayHints'.

Currently, it provides type information to 'var' and 'let' variables. In
the future, inlay hints can also be added for 'const' and for function
parameters. The protocol also reserves space for a tooltip field, which
is not used, yet, but support for it can be added in the future, without
further changing the protocol.

The change includes refactoring to allow the 'inlayHints' command to
return a completely different structure, compared to the other
nimsuggest commands. This will allow other future commands to have
custom return types as well. All the previous commands return the same
structure as before, so perfect backwards compatibility is maintained.

To use this feature, an update to the nim language server, as well as
the VS code extension is needed.

Related PRs:
nimlangserver: https://github.com/nim-lang/langserver/pull/53
VS code extension: https://github.com/saem/vscode-nim/pull/134

---------

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
This commit is contained in:
Nikolay Nikolov
2023-11-04 09:51:09 +02:00
committed by GitHub
parent 95e5ad6927
commit 3f2b9c8bcf
6 changed files with 149 additions and 44 deletions

View File

@@ -903,6 +903,7 @@ type
info*: TLineInfo
when defined(nimsuggest):
endInfo*: TLineInfo
hasUserSpecifiedType*: bool # used for determining whether to display inlay type hints
owner*: PSym
flags*: TSymFlags
ast*: PNode # syntax tree of proc, iterator, etc.:

View File

@@ -57,6 +57,7 @@ type
SymInfoPair* = object
sym*: PSym
info*: TLineInfo
isDecl*: bool
PipelinePass* = enum
NonePass

View File

@@ -199,7 +199,7 @@ type
IdeCmd* = enum
ideNone, ideSug, ideCon, ideDef, ideUse, ideDus, ideChk, ideChkFile, ideMod,
ideHighlight, ideOutline, ideKnown, ideMsg, ideProject, ideGlobalSymbols,
ideRecompile, ideChanged, ideType, ideDeclaration, ideExpand
ideRecompile, ideChanged, ideType, ideDeclaration, ideExpand, ideInlayHints
Feature* = enum ## experimental features; DO NOT RENAME THESE!
dotOperators,
@@ -288,9 +288,24 @@ type
version*: int
endLine*: uint16
endCol*: int
inlayHintInfo*: SuggestInlayHint
Suggestions* = seq[Suggest]
SuggestInlayHintKind* = enum
sihkType = "Type",
sihkParameter = "Parameter"
SuggestInlayHint* = ref object
kind*: SuggestInlayHintKind
line*: int # Starts at 1
column*: int # Starts at 0
label*: string
paddingLeft*: bool
paddingRight*: bool
allowInsert*: bool
tooltip*: string
ProfileInfo* = object
time*: float
count*: int
@@ -1071,6 +1086,7 @@ proc `$`*(c: IdeCmd): string =
of ideRecompile: "recompile"
of ideChanged: "changed"
of ideType: "type"
of ideInlayHints: "inlayHints"
proc floatInt64Align*(conf: ConfigRef): int16 =
## Returns either 4 or 8 depending on reasons.

View File

@@ -672,9 +672,11 @@ proc semVarOrLet(c: PContext, n: PNode, symkind: TSymKind): PNode =
addToVarSection(c, result, b)
continue
var hasUserSpecifiedType = false
var typ: PType = nil
if a[^2].kind != nkEmpty:
typ = semTypeNode(c, a[^2], nil)
hasUserSpecifiedType = true
var typFlags: TTypeAllowedFlags = {}
@@ -746,6 +748,8 @@ proc semVarOrLet(c: PContext, n: PNode, symkind: TSymKind): PNode =
addToVarSection(c, result, n, a)
continue
var v = semIdentDef(c, a[j], symkind, false)
when defined(nimsuggest):
v.hasUserSpecifiedType = hasUserSpecifiedType
styleCheckDef(c, v)
onDef(a[j].info, v)
if sfGenSym notin v.flags:

View File

@@ -107,7 +107,7 @@ proc getTokenLenFromSource(conf: ConfigRef; ident: string; info: TLineInfo): int
result = 0
elif ident[0] in linter.Letters and ident[^1] != '=':
result = identLen(line, column)
if cmpIgnoreStyle(line[column..column + result - 1], ident) != 0:
if cmpIgnoreStyle(line[column..column + result - 1], ident[0..min(result-1,len(ident)-1)]) != 0:
result = 0
else:
var sourceIdent: string = ""
@@ -177,7 +177,7 @@ proc symToSuggest*(g: ModuleGraph; s: PSym, isLocal: bool, section: IdeCmd, info
result.filePath = toFullPath(g.config, infox)
result.line = toLinenumber(infox)
result.column = toColumn(infox)
result.tokenLen = if section != ideHighlight:
result.tokenLen = if section notin {ideHighlight, ideInlayHints}:
s.name.s.len
else:
getTokenLenFromSource(g.config, s.name.s, infox)
@@ -185,50 +185,82 @@ proc symToSuggest*(g: ModuleGraph; s: PSym, isLocal: bool, section: IdeCmd, info
result.endLine = endLine
result.endCol = endCol
proc `$`*(suggest: Suggest): string =
result = $suggest.section
proc `$`*(suggest: SuggestInlayHint): string =
result = $suggest.kind
result.add(sep)
if suggest.section == ideHighlight:
if suggest.symkind.TSymKind == skVar and suggest.isGlobal:
result.add("skGlobalVar")
elif suggest.symkind.TSymKind == skLet and suggest.isGlobal:
result.add("skGlobalLet")
result.add($suggest.line)
result.add(sep)
result.add($suggest.column)
result.add(sep)
result.add(suggest.label)
result.add(sep)
result.add($suggest.paddingLeft)
result.add(sep)
result.add($suggest.paddingRight)
result.add(sep)
result.add($suggest.allowInsert)
result.add(sep)
result.add(suggest.tooltip)
proc `$`*(suggest: Suggest): string =
if suggest.section == ideInlayHints:
result = $suggest.inlayHintInfo
else:
result = $suggest.section
result.add(sep)
if suggest.section == ideHighlight:
if suggest.symkind.TSymKind == skVar and suggest.isGlobal:
result.add("skGlobalVar")
elif suggest.symkind.TSymKind == skLet and suggest.isGlobal:
result.add("skGlobalLet")
else:
result.add($suggest.symkind.TSymKind)
result.add(sep)
result.add($suggest.line)
result.add(sep)
result.add($suggest.column)
result.add(sep)
result.add($suggest.tokenLen)
else:
result.add($suggest.symkind.TSymKind)
result.add(sep)
result.add($suggest.line)
result.add(sep)
result.add($suggest.column)
result.add(sep)
result.add($suggest.tokenLen)
else:
result.add($suggest.symkind.TSymKind)
result.add(sep)
if suggest.qualifiedPath.len != 0:
result.add(suggest.qualifiedPath.join("."))
result.add(sep)
result.add(suggest.forth)
result.add(sep)
result.add(suggest.filePath)
result.add(sep)
result.add($suggest.line)
result.add(sep)
result.add($suggest.column)
result.add(sep)
when defined(nimsuggest) and not defined(noDocgen) and not defined(leanCompiler):
result.add(suggest.doc.escape)
if suggest.version == 0 or suggest.version == 3:
result.add(sep)
result.add($suggest.quality)
if suggest.section == ideSug:
if suggest.qualifiedPath.len != 0:
result.add(suggest.qualifiedPath.join("."))
result.add(sep)
result.add(suggest.forth)
result.add(sep)
result.add(suggest.filePath)
result.add(sep)
result.add($suggest.line)
result.add(sep)
result.add($suggest.column)
result.add(sep)
when defined(nimsuggest) and not defined(noDocgen) and not defined(leanCompiler):
result.add(suggest.doc.escape)
if suggest.version == 0 or suggest.version == 3:
result.add(sep)
result.add($suggest.prefix)
result.add($suggest.quality)
if suggest.section == ideSug:
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)
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 suggestToSuggestInlayHint*(sug: Suggest): SuggestInlayHint =
SuggestInlayHint(
kind: sihkType,
line: sug.line,
column: sug.column + sug.tokenLen,
label: ": " & sug.forth,
paddingLeft: false,
paddingRight: false,
allowInsert: true,
tooltip: ""
)
proc suggestResult*(conf: ConfigRef; s: Suggest) =
if not isNil(conf.suggestionResultHook):
@@ -537,7 +569,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)
g.suggestSymbols.mgetOrPut(info.fileIndex, @[]).add SymInfoPair(sym: s, info: info, isDecl: isDecl)
if conf.suggestVersion == 0:
if s.allUsages.len == 0:

View File

@@ -161,7 +161,7 @@ proc listEpc(): SexpNode =
argspecs = sexp("file line column dirtyfile".split(" ").map(newSSymbol))
docstring = sexp("line starts at 1, column at 0, dirtyfile is optional")
result = newSList()
for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration"]:
for command in ["sug", "con", "def", "use", "dus", "chk", "mod", "globalSymbols", "recompile", "saved", "chkFile", "declaration", "inlayHints"]:
let
cmd = sexp(command)
methodDesc = newSList()
@@ -506,6 +506,7 @@ proc execCmd(cmd: string; graph: ModuleGraph; cachedMsgs: CachedMsgs) =
of "chkfile": conf.ideCmd = ideChkFile
of "recompile": conf.ideCmd = ideRecompile
of "type": conf.ideCmd = ideType
of "inlayhints": conf.ideCmd = ideInlayHints
else: err()
var dirtyfile = ""
var orig = ""
@@ -774,6 +775,18 @@ proc findSymData(graph: ModuleGraph, trackPos: TLineInfo):
result[] = s
break
func isInRange*(current, startPos, endPos: TLineInfo, tokenLen: int): bool =
result = current.fileIndex == startPos.fileIndex and
(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)
proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
ref SymInfoPair =
let
@@ -781,6 +794,14 @@ proc findSymData(graph: ModuleGraph, file: AbsoluteFile; line, col: int):
trackPos = newLineInfo(fileIdx, line, col)
result = findSymData(graph, trackPos)
proc findSymDataInRange(graph: ModuleGraph, file: AbsoluteFile; startLine, startCol, endLine, endCol: int):
seq[SymInfoPair] =
let
fileIdx = fileInfoIdx(graph.config, file)
startPos = newLineInfo(fileIdx, startLine, startCol)
endPos = newLineInfo(fileIdx, endLine, endCol)
result = findSymDataInRange(graph, startPos, endPos)
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:
@@ -803,6 +824,23 @@ proc suggestResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
endLine = endLine, endCol = endCol)
suggestResult(graph.config, suggest)
proc suggestInlayHintResult(graph: ModuleGraph, sym: PSym, info: TLineInfo,
defaultSection = ideNone, endLine: uint16 = 0, endCol = 0) =
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 = suggestToSuggestInlayHint(suggestDef)
suggestDef.section = ideInlayHints
if sym.kind == skForVar:
suggestDef.inlayHintInfo.allowInsert = false
suggestResult(graph.config, suggestDef)
const
# kinds for ideOutline and ideGlobalSymbols
searchableSymKinds = {skField, skEnumField, skIterator, skMethod, skFunc, skProc, skConverter, skTemplate}
@@ -910,7 +948,7 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
graph.markDirtyIfNeeded(dirtyFile.string, fileInfoIdx(conf, file))
# these commands require fully compiled project
if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk} and graph.needsCompilation():
if cmd in {ideUse, ideDus, ideGlobalSymbols, ideChk, ideInlayHints} and graph.needsCompilation():
graph.recompilePartially()
# when doing incremental build for the project root we should make sure that
# everything is unmarked as no longer beeing dirty in case there is no
@@ -1066,6 +1104,19 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
graph.markDirty fileIndex
graph.markClientsDirty fileIndex
of ideInlayHints:
myLog fmt "Executing inlayHints"
var endLine = 0
var endCol = -1
var i = 0
i += skipWhile(tag, seps, i)
i += parseInt(tag, endLine, i)
i += skipWhile(tag, seps, i)
i += parseInt(tag, endCol, i)
let s = graph.findSymDataInRange(file, line, col, endLine, endCol)
for q in s:
if q.sym.kind in {skLet, skVar, skForVar} and q.isDecl and not q.sym.hasUserSpecifiedType:
graph.suggestInlayHintResult(q.sym, q.info, ideInlayHints)
else:
myLog fmt "Discarding {cmd}"