Skip to content

Commit

Permalink
Inlay hints support (nim-lang#22896)
Browse files Browse the repository at this point in the history
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: nim-lang/langserver#53
VS code extension: saem/vscode-nim#134

---------

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
(cherry picked from commit 3f2b9c8)
  • Loading branch information
nickysn committed Nov 7, 2023
1 parent 0779a5e commit 8da23e9
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 44 deletions.
1 change: 1 addition & 0 deletions compiler/ast.nim
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,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.:
Expand Down
1 change: 1 addition & 0 deletions compiler/modulegraphs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type
SymInfoPair* = object
sym*: PSym
info*: TLineInfo
isDecl*: bool

ModuleGraph* {.acyclic.} = ref object
ifaces*: seq[Iface] ## indexed by int32 fileIdx
Expand Down
18 changes: 17 additions & 1 deletion compiler/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,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!
implicitDeref,
Expand Down Expand Up @@ -266,9 +266,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
Expand Down Expand Up @@ -1035,6 +1050,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.
Expand Down
4 changes: 4 additions & 0 deletions compiler/semstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,11 @@ proc semVarOrLet(c: PContext, n: PNode, symkind: TSymKind): PNode =
if a.kind notin {nkIdentDefs, nkVarTuple}: illFormedAst(a, c.config)
checkMinSonsLen(a, 3, c.config)

var hasUserSpecifiedType = false
var typ: PType = nil
if a[^2].kind != nkEmpty:
typ = semTypeNode(c, a[^2], nil)
hasUserSpecifiedType = true

var typFlags: TTypeAllowedFlags

Expand Down Expand Up @@ -644,6 +646,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:
Expand Down
114 changes: 73 additions & 41 deletions compiler/suggest.nim
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,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
Expand Down Expand Up @@ -173,58 +173,90 @@ 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)
result.version = g.config.suggestVersion
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")
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)
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.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 = $suggest.section
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 in {0, 3}:
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.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 in {0, 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):
Expand Down Expand Up @@ -521,7 +553,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:
Expand Down
55 changes: 53 additions & 2 deletions nimsuggest/nimsuggest.nim
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,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()
Expand Down Expand Up @@ -478,6 +478,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 = ""
Expand Down Expand Up @@ -747,13 +748,33 @@ 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
fileIdx = fileInfoIdx(graph.config, file)
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:
Expand All @@ -776,6 +797,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}
Expand Down Expand Up @@ -883,7 +921,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
Expand Down Expand Up @@ -1039,6 +1077,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}"

Expand Down

0 comments on commit 8da23e9

Please sign in to comment.