From 3f2b9c8bcf9faf30b6844e5222ffe6ec501064e8 Mon Sep 17 00:00:00 2001 From: Nikolay Nikolov Date: Sat, 4 Nov 2023 09:51:09 +0200 Subject: [PATCH] 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 --- compiler/ast.nim | 1 + compiler/modulegraphs.nim | 1 + compiler/options.nim | 18 +++++- compiler/semstmts.nim | 4 ++ compiler/suggest.nim | 114 ++++++++++++++++++++++++-------------- nimsuggest/nimsuggest.nim | 55 +++++++++++++++++- 6 files changed, 149 insertions(+), 44 deletions(-) diff --git a/compiler/ast.nim b/compiler/ast.nim index 5ee9afa024..661a82703c 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -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.: diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index ba636eb5a1..5c23325e82 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -57,6 +57,7 @@ type SymInfoPair* = object sym*: PSym info*: TLineInfo + isDecl*: bool PipelinePass* = enum NonePass diff --git a/compiler/options.nim b/compiler/options.nim index f2d93a9b3a..704248d789 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -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. diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim index a4de874ba9..0196b9d03c 100644 --- a/compiler/semstmts.nim +++ b/compiler/semstmts.nim @@ -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: diff --git a/compiler/suggest.nim b/compiler/suggest.nim index 5714c6d211..1d84fada52 100644 --- a/compiler/suggest.nim +++ b/compiler/suggest.nim @@ -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: diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim index 28faf9e3c3..3834acb93c 100644 --- a/nimsuggest/nimsuggest.nim +++ b/nimsuggest/nimsuggest.nim @@ -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}"