Skip to content

Commit

Permalink
added basic inlay hint support
Browse files Browse the repository at this point in the history
  • Loading branch information
Nimaoth committed Mar 10, 2024
1 parent 30befe7 commit 0fb949e
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 30 deletions.
6 changes: 6 additions & 0 deletions src/language_server_absytree_commands.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,9 @@ method getCompletions*(self: LanguageServerAbsytreeCommands, languageId: string,
method getSymbols*(self: LanguageServerAbsytreeCommands, filename: string): Future[seq[Symbol]] {.async.} =
var completions: seq[Symbol]
return completions

method getHover*(self: LanguageServerAbsytreeCommands, filename: string, location: Cursor): Future[Option[string]] {.async.} =
return string.none

method getInlayHints*(self: LanguageServerAbsytreeCommands, filename: string, selection: Selection): Future[seq[InlayHint]] {.async.} =
return newSeq[InlayHint]()
19 changes: 18 additions & 1 deletion src/text/language/language_server_base.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[options, tables]
import std/[options, tables, json]
import misc/[custom_async, custom_logger]
import scripting_api except DocumentEditor, TextDocumentEditor, AstDocumentEditor
import workspaces/workspace
Expand All @@ -12,6 +12,12 @@ type LanguageServer* = ref object of RootObj
onRequestSaveIndex: Table[string, seq[OnRequestSaveHandle]]

type SymbolType* = enum Unknown, Procedure, Function, MutableVariable, ImmutableVariable, Constant, Parameter, Type
type InlayHintKind* = enum Type, Parameter

type TextEdit* = object
selection*: Selection
newText*: string


type Definition* = object
location*: Cursor
Expand All @@ -32,6 +38,16 @@ type TextCompletion* = object
typ*: string
doc*: string

type InlayHint* = object
location*: Cursor
label*: string # | InlayHintLabelPart[] # todo
kind*: Option[InlayHintKind]
textEdits*: seq[TextEdit]
tooltip*: Option[string] # | MarkupContent # todo
paddingLeft*: bool
paddingRight*: bool
data*: Option[JsonNode]

var getOrCreateLanguageServer*: proc(languageId: string, filename: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none, workspace = WorkspaceFolder.none): Future[Option[LanguageServer]] = nil

method start*(self: LanguageServer): Future[void] {.base.} = discard
Expand All @@ -44,6 +60,7 @@ method getCompletions*(self: LanguageServer, languageId: string, filename: strin
method saveTempFile*(self: LanguageServer, filename: string, content: string): Future[void] {.base.} = discard
method getSymbols*(self: LanguageServer, filename: string): Future[seq[Symbol]] {.base.} = discard
method getHover*(self: LanguageServer, filename: string, location: Cursor): Future[Option[string]] {.base.} = discard
method getInlayHints*(self: LanguageServer, filename: string, selection: Selection): Future[seq[InlayHint]] {.base.} = discard

var handleIdCounter = 1

Expand Down
33 changes: 32 additions & 1 deletion src/text/language/language_server_lsp.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[strutils, options, json, tables, uri, strformat]
import std/[strutils, options, json, tables, uri, strformat, sugar, sequtils]
import scripting_api except DocumentEditor, TextDocumentEditor, AstDocumentEditor
import misc/[event, util, custom_logger, custom_async, myjsonutils]
import text/text_editor
Expand All @@ -21,6 +21,8 @@ proc deinitLanguageServers*() =

proc toPosition*(cursor: Cursor): Position = Position(line: cursor.line, character: cursor.column)
proc toRange*(selection: Selection): Range = Range(start: selection.first.toPosition, `end`: selection.last.toPosition)
proc toCursor*(position: Position): Cursor = (position.line, position.character)
proc toSelection*(`range`: Range): Selection = (`range`.start.toCursor, `range`.`end`.toCursor)

method connect*(self: LanguageServerLSP) =
log lvlInfo, fmt"Connecting document"
Expand Down Expand Up @@ -204,6 +206,35 @@ method getHover*(self: LanguageServerLSP, filename: string, location: Cursor): F

return string.none

method getInlayHints*(self: LanguageServerLSP, filename: string, selection: Selection): Future[seq[language_server_base.InlayHint]] {.async.} =
let response = await self.client.getInlayHints(filename, selection)
if response.isError:
log(lvlError, fmt"Error: {response.error}")
return newSeq[language_server_base.InlayHint]()

let parsedResponse = response.result

if parsedResponse.getSome(inlayHints):
var hints: seq[language_server_base.InlayHint]
for hint in inlayHints:
hints.add language_server_base.InlayHint(
location: (hint.position.line, hint.position.character),
label: hint.label,
kind: hint.kind.mapIt(case it
of lsp_types.InlayHintKind.Type: language_server_base.InlayHintKind.Type
of lsp_types.InlayHintKind.Parameter: language_server_base.InlayHintKind.Parameter
),
textEdits: hint.textEdits.mapIt(language_server_base.TextEdit(selection: it.`range`.toSelection, newText: it.newText)),
# tooltip*: Option[string] # | MarkupContent # todo
paddingLeft: hint.paddingLeft.get(false),
paddingRight: hint.paddingRight.get(false),
data: hint.data
)

return hints

return newSeq[language_server_base.InlayHint]()

method getSymbols*(self: LanguageServerLSP, filename: string): Future[seq[Symbol]] {.async.} =
var completions: seq[Symbol]

Expand Down
21 changes: 21 additions & 0 deletions src/text/language/lsp_client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,27 @@ proc getHover*(client: LSPClient, filename: string, line: int, column: int): Fut

return (await client.sendRequest("textDocument/hover", params)).to DocumentHoverResponse

proc getInlayHints*(client: LSPClient, filename: string, selection: ((int, int), (int, int))): Future[Response[InlayHintResponse]] {.async.} =
let path = client.translatePath(filename).await

client.cancelAllOf("textDocument/inlayHint")

let params = InlayHintParams(
textDocument: TextDocumentIdentifier(uri: $path.toUri),
`range`: Range(
start: Position(
line: selection[0][0],
character: selection[0][1],
),
`end`: Position(
line: selection[1][0],
character: selection[1][1],
),
)
).toJson

return (await client.sendRequest("textDocument/inlayHint", params)).to InlayHintResponse

proc getSymbols*(client: LSPClient, filename: string): Future[Response[DocumentSymbolResponse]] {.async.} =
# debugf"[getSymbols] {filename.absolutePath}:{line}:{column}"
let path = client.translatePath(filename).await
Expand Down
20 changes: 20 additions & 0 deletions src/text/language/lsp_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ type
AsIs = 1
AdjustIndentation = 2

InlayHintKind* {.pure.} = enum
Type = 1
Parameter = 2

ServerInfo* = object
name*: string
version*: string
Expand Down Expand Up @@ -548,6 +552,21 @@ type
contents*: HoverContentVariant
range*: Option[Range]

InlayHintParams* = object
workDoneProgress*: bool
textDocument*: TextDocumentIdentifier
range*: Range

InlayHint* = object
position*: Position
label*: string # | InlayHintLabelPart[] # todo
kind*: Option[InlayHintKind]
textEdits*: seq[TextEdit]
tooltip*: Option[string] # | MarkupContent # todo
paddingLeft*: Option[bool]
paddingRight*: Option[bool]
data*: Option[JsonNode]

variant(CompletionResponseVariant, seq[CompletionItem], CompletionList)
variant(DefinitionResponseVariant, Location, seq[Location], seq[LocationLink])
variant(DeclarationResponseVariant, Location, seq[Location], seq[LocationLink])
Expand All @@ -558,6 +577,7 @@ type CompletionResponse* = CompletionResponseVariant
type DefinitionResponse* = DefinitionResponseVariant
type DeclarationResponse* = DeclarationResponseVariant
type DocumentSymbolResponse* = DocumentSymbolResponseVariant
type InlayHintResponse* = Option[seq[InlayHint]]

variant(TextDocumentSyncVariant, TextDocumentSyncOptions, TextDocumentSyncKind)
variant(HoverProviderVariant, bool, HoverOptions)
Expand Down
20 changes: 19 additions & 1 deletion src/text/text_document.nim
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type StyledText* = object
bounds*: Rect
opacity*: Option[float]
joinNext*: bool
textRange*: Option[tuple[startOffset: int, endOffset: int, startIndex: RuneIndex, endIndex: RuneIndex]]

type StyledLine* = ref object
index*: int
Expand Down Expand Up @@ -282,6 +283,14 @@ proc splitPartAt*(line: var StyledLine, partIndex: int, index: RuneIndex) =
var copy = line.parts[partIndex]
let byteIndex = line.parts[partIndex].text.toOpenArray.runeOffset(index)
line.parts[partIndex].text = line.parts[partIndex].text[0..<byteIndex]
if line.parts[partIndex].textRange.isSome:
let byteIndexGlobal = line.parts[partIndex].textRange.get.startOffset + byteIndex
let indexGlobal = line.parts[partIndex].textRange.get.startIndex + index.RuneCount
line.parts[partIndex].textRange.get.endOffset = byteIndexGlobal
line.parts[partIndex].textRange.get.endIndex = indexGlobal
copy.textRange.get.startOffset = byteIndexGlobal
copy.textRange.get.startIndex = indexGlobal

copy.text = copy.text[byteIndex..^1]
line.parts.insert(copy, partIndex + 1)

Expand Down Expand Up @@ -335,12 +344,21 @@ proc overrideStyle*(self: TextDocument, line: var StyledLine, first: int, last:
proc overrideStyleAndText*(self: TextDocument, line: var StyledLine, first: int, text: string, scope: string, priority: int, opacity: Option[float] = float.none, joinNext: bool = false) =
line.overrideStyleAndText(self.lines[line.index].toOpenArray.runeIndex(first, returnLen=true), text, scope, priority, opacity, joinNext)

proc insertText*(self: TextDocument, line: var StyledLine, offset: RuneIndex, text: string, scope: string) =
line.splitAt(offset)
var index = 0.RuneIndex
for i in 0..line.parts.high:
if offset == index + line.parts[i].text.runeLen:
line.parts.insert(StyledText(text: text, scope: scope, scopeC: scope.cstring, priority: 1000000000), i + 1)
return
index += line.parts[i].text.runeLen

proc getStyledText*(self: TextDocument, i: int): StyledLine =
if self.styledTextCache.contains(i):
result = self.styledTextCache[i]
else:
var line = self.lines[i]
result = StyledLine(index: i, parts: @[StyledText(text: line, scope: "", priority: 1000000000)])
result = StyledLine(index: i, parts: @[StyledText(text: line, scope: "", scopeC: "", priority: 1000000000, textRange: (0, line.len, 0.RuneIndex, line.runeLen.RuneIndex).some)])
self.styledTextCache[i] = result

var regexes = initTable[string, Regex]()
Expand Down
47 changes: 45 additions & 2 deletions src/text/text_editor.nim
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type TextDocumentEditor* = ref object of DocumentEditor
hoverLocation*: Cursor # where to show the hover info
hoverScrollOffset*: float # the scroll offset inside the hover window

# inline hints
inlayHints: seq[InlayHint]
inlayHintsTask: DelayedTask

completionEventHandler: EventHandler
modeEventHandler: EventHandler
currentMode*: string
Expand Down Expand Up @@ -108,6 +112,7 @@ proc refilterCompletions(self: TextDocumentEditor)
proc getSelectionForMove*(self: TextDocumentEditor, cursor: Cursor, move: string, count: int = 0): Selection
proc extendSelectionWithMove*(self: TextDocumentEditor, selection: Selection, move: string, count: int = 0): Selection
proc updateTargetColumn*(self: TextDocumentEditor, cursor: SelectionCursor)
proc updateInlayHints*(self: TextDocumentEditor)

proc clampCursor*(self: TextDocumentEditor, cursor: Cursor): Cursor = self.document.clampCursor(cursor)

Expand Down Expand Up @@ -274,6 +279,7 @@ proc centerCursor*(self: TextDocumentEditor, cursor: Cursor) =

self.previousBaseIndex = cursor.line
self.scrollOffset = self.lastContentBounds.h * 0.5 - self.platform.totalLineHeight * 0.5
self.updateInlayHints()

self.markDirty()

Expand Down Expand Up @@ -306,6 +312,7 @@ proc scrollToCursor*(self: TextDocumentEditor, cursor: Cursor, margin: Option[fl
self.scrollOffset = self.lastContentBounds.h - margin - totalLineHeight
self.previousBaseIndex = targetLine

self.updateInlayHints()
self.markDirty()

proc getContextWithMode*(self: TextDocumentEditor, context: string): string
Expand Down Expand Up @@ -424,6 +431,11 @@ proc screenLineCount(self: TextDocumentEditor): int {.expose: "editor.text".} =
## This value depends on the size of the view this editor is in and the font size
return (self.lastContentBounds.h / self.platform.totalLineHeight).int

proc visibleTextRange*(self: TextDocumentEditor, buffer: int = 0): Selection =
result.first.line = max(0, self.previousBaseIndex - int(self.scrollOffset / self.platform.totalLineHeight) - buffer)
result.last.line = min(self.lineCount - 1, self.previousBaseIndex - int(self.scrollOffset / self.platform.totalLineHeight) + self.screenLineCount + buffer)
result.last.column = self.document.lastValidIndex(result.last.line)

proc doMoveCursorColumn(self: TextDocumentEditor, cursor: Cursor, offset: int, wrap: bool = true): Cursor {.expose: "editor.text".} =
var cursor = cursor
var column = cursor.column
Expand Down Expand Up @@ -791,10 +803,11 @@ proc pasteAsync*(self: TextDocumentEditor, register: string): Future[void] {.asy
proc paste*(self: TextDocumentEditor, register: string = "") {.expose("editor.text").} =
asyncCheck self.pasteAsync(register)

proc scrollText(self: TextDocumentEditor, amount: float32) {.expose("editor.text").} =
proc scrollText*(self: TextDocumentEditor, amount: float32) {.expose("editor.text").} =
if self.disableScrolling:
return
self.scrollOffset += amount
self.updateInlayHints()
self.markDirty()

proc scrollLines(self: TextDocumentEditor, amount: int) {.expose("editor.text").} =
Expand All @@ -807,6 +820,7 @@ proc scrollLines(self: TextDocumentEditor, amount: int) {.expose("editor.text").
while self.previousBaseIndex >= self.screenLineCount - 1:
self.previousBaseIndex.dec
self.scrollOffset -= self.platform.totalLineHeight
self.updateInlayHints()
self.markDirty()

proc duplicateLastSelection*(self: TextDocumentEditor) {.expose("editor.text").} =
Expand Down Expand Up @@ -926,6 +940,7 @@ proc setCursorScrollOffset*(self: TextDocumentEditor, offset: float, cursor: Sel
let line = self.getCursor(cursor).line
self.previousBaseIndex = line
self.scrollOffset = offset
self.updateInlayHints()
self.markDirty()

proc getContentBounds*(self: TextDocumentEditor): Vec2 {.expose("editor.text").} =
Expand Down Expand Up @@ -1585,6 +1600,23 @@ proc showHoverForDelayed*(self: TextDocumentEditor, cursor: Cursor) =

self.markDirty()

proc updateInlayHintsAsync*(self: TextDocumentEditor): Future[void] {.async.} =
let languageServer = await self.document.getLanguageServer()
if languageServer.getSome(ls):
let inlayHints = await ls.getInlayHints(self.document.fullPath, self.visibleTextRange(buffer = 10))
# todo: detect if canceled instead
if inlayHints.len > 0:
log lvlInfo, fmt"Updating inlay hints: {inlayHints}"
self.inlayHints = inlayHints
self.markDirty()

proc updateInlayHints*(self: TextDocumentEditor) =
if self.inlayHintsTask.isNil:
self.inlayHintsTask = startDelayed(200, repeat=false):
asyncCheck self.updateInlayHintsAsync()
else:
self.inlayHintsTask.reschedule()

proc isRunningSavedCommands*(self: TextDocumentEditor): bool {.expose("editor.text").} = self.bIsRunningSavedCommands

proc runSavedCommands*(self: TextDocumentEditor) {.expose("editor.text").} =
Expand Down Expand Up @@ -1756,7 +1788,7 @@ genDispatcher("editor.text")
# addGlobalDispatchTable "editor.text", genDispatchTable("editor.text")

proc getStyledText*(self: TextDocumentEditor, i: int): StyledLine =
result = self.document.getStyledText(i)
result = StyledLine(index: i, parts: self.document.getStyledText(i).parts)

let chars = (self.lastTextAreaBounds.w / self.platform.charWidth - 2).RuneCount
if chars > 0.RuneCount:
Expand All @@ -1774,6 +1806,14 @@ proc getStyledText*(self: TextDocumentEditor, i: int): StyledLine =
self.document.splitAt(result, override.cursor.column + override.text.len)
self.document.overrideStyleAndText(result, override.cursor.column, override.text, override.scope, -2, joinNext = true)

var offset = 0.RuneCount
for inlayHint in self.inlayHints:
if inlayHint.location.line != i:
continue

self.document.insertText(result, inlayHint.location.column.RuneIndex + offset, inlayHint.label, "comment")
offset += inlayHint.label.runeLen

proc handleActionInternal(self: TextDocumentEditor, action: string, args: JsonNode): EventResponse =
# debugf"[textedit] handleAction {action}, '{args}'"

Expand Down Expand Up @@ -1913,6 +1953,8 @@ proc handleTextDocumentTextChanged(self: TextDocumentEditor) =
if self.showCompletions:
self.refilterCompletions()

self.updateInlayHints()

self.markDirty()

proc handleTextDocumentLoaded(self: TextDocumentEditor) =
Expand All @@ -1931,6 +1973,7 @@ proc createTextEditorInstance(): TextDocumentEditor =
editor.cursorsId = newId()
editor.completionsId = newId()
editor.hoverId = newId()
editor.inlayHints = @[]
return editor

proc newTextEditor*(document: TextDocument, app: AppInterface, configProvider: ConfigProvider): TextDocumentEditor =
Expand Down
Loading

0 comments on commit 0fb949e

Please sign in to comment.