diff --git a/src/language_server_absytree_commands.nim b/src/language_server_absytree_commands.nim index d5d26ce8..2f45e052 100644 --- a/src/language_server_absytree_commands.nim +++ b/src/language_server_absytree_commands.nim @@ -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]() \ No newline at end of file diff --git a/src/text/language/language_server_base.nim b/src/text/language/language_server_base.nim index 71a9fa66..f35be604 100644 --- a/src/text/language/language_server_base.nim +++ b/src/text/language/language_server_base.nim @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/text/language/language_server_lsp.nim b/src/text/language/language_server_lsp.nim index 8da86362..7cecdca4 100644 --- a/src/text/language/language_server_lsp.nim +++ b/src/text/language/language_server_lsp.nim @@ -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 @@ -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" @@ -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] diff --git a/src/text/language/lsp_client.nim b/src/text/language/lsp_client.nim index b74a4e53..7c7709fb 100644 --- a/src/text/language/lsp_client.nim +++ b/src/text/language/lsp_client.nim @@ -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 diff --git a/src/text/language/lsp_types.nim b/src/text/language/lsp_types.nim index 8fd10add..1ce40b4b 100644 --- a/src/text/language/lsp_types.nim +++ b/src/text/language/lsp_types.nim @@ -124,6 +124,10 @@ type AsIs = 1 AdjustIndentation = 2 + InlayHintKind* {.pure.} = enum + Type = 1 + Parameter = 2 + ServerInfo* = object name*: string version*: string @@ -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]) @@ -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) diff --git a/src/text/text_document.nim b/src/text/text_document.nim index cbbcf94f..3d2e70cc 100644 --- a/src/text/text_document.nim +++ b/src/text/text_document.nim @@ -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 @@ -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..= self.screenLineCount - 1: self.previousBaseIndex.dec self.scrollOffset -= self.platform.totalLineHeight + self.updateInlayHints() self.markDirty() proc duplicateLastSelection*(self: TextDocumentEditor) {.expose("editor.text").} = @@ -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").} = @@ -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").} = @@ -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: @@ -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}'" @@ -1913,6 +1953,8 @@ proc handleTextDocumentTextChanged(self: TextDocumentEditor) = if self.showCompletions: self.refilterCompletions() + self.updateInlayHints() + self.markDirty() proc handleTextDocumentLoaded(self: TextDocumentEditor) = @@ -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 = diff --git a/src/ui/widget_builder_text_document.nim b/src/ui/widget_builder_text_document.nim index c4631015..7ea5fa40 100644 --- a/src/ui/widget_builder_text_document.nim +++ b/src/ui/widget_builder_text_document.nim @@ -73,12 +73,11 @@ proc renderLine*( var subLine: UINode = nil var start = 0 - var startRune = 0.RuneCount var lastPartXW: float32 = 0 var partIndex = 0 var subLineIndex = 0 var subLinePartIndex = 0 - while partIndex < line.parts.len: + while partIndex < line.parts.len: # outer loop for wrapped lines within this line builder.panel(flagsInner + LayoutHorizontal): subLine = currentNode @@ -89,7 +88,7 @@ proc renderLine*( builder.panel(&{DrawText, SizeToContentX, SizeToContentY}, text = lineNumberText, x = lineNumberX, textColor = textColor) lastPartXW = lineNumberTotalWidth - while partIndex < line.parts.len: + while partIndex < line.parts.len: # inner loop for parts within a wrapped line part template part: StyledText = line.parts[partIndex] let partRuneLen = part.text.runeLen @@ -115,14 +114,39 @@ proc renderLine*( let textColor = if part.scope.len == 0: textColor else: theme.tokenColor(part, textColor) + var startRune = 0.RuneIndex + if part.textRange.isSome: + startRune = part.textRange.get.startIndex + else: + # Inlay text, find start rune of neighbor, prefer left side + var found = false + for i in countdown(partIndex - 1, 0): + if line.parts[i].textRange.isSome: + startRune = line.parts[i].textRange.get.endIndex + found = true + break + + if not found: + for i in countup(partIndex + 1, line.parts.high): + if line.parts[i].textRange.isSome: + startRune = line.parts[i].textRange.get.startIndex + break + # Find background color var colorIndex = 0 - while colorIndex < backgroundColors.high and (backgroundColors[colorIndex].first == backgroundColors[colorIndex].last or backgroundColors[colorIndex].last <= startRune): - inc colorIndex + if part.textRange.isNone: + # prefer color of left neighbor for inlay text + while colorIndex < backgroundColors.high and (backgroundColors[colorIndex].first == backgroundColors[colorIndex].last or backgroundColors[colorIndex].last <= startRune - 1.RuneCount): + inc colorIndex + else: + while colorIndex < backgroundColors.high and (backgroundColors[colorIndex].first == backgroundColors[colorIndex].last or backgroundColors[colorIndex].last <= startRune): + inc colorIndex var backgroundColor = backgroundColor var addBackgroundAsChildren = true - if backgroundColors[colorIndex].last >= startRune.RuneIndex + partRuneLen: + + # check if fully covered by background color (inlay text is always fully covered by background color) + if part.textRange.isNone or backgroundColors[colorIndex].last >= startRune + partRuneLen: backgroundColor = backgroundColors[colorIndex].color addBackgroundAsChildren = false @@ -139,15 +163,17 @@ proc renderLine*( let text = if addBackgroundAsChildren: "" else: part.text let textRuneLen = part.text.runeLen.int + let isInlay = part.textRange.isNone + var partNode: UINode builder.panel(partFlags, text = text, backgroundColor = backgroundColor, textColor = textColor): partNode = currentNode - capture line, partNode, startRune, textRuneLen: + capture line, partNode, startRune, textRuneLen, isInlay: onClickAny btn: self.lastPressedMouseButton = btn if btn == Left: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, if isInlay: vec2() else: pos) self.selection = (line.index, offset).toSelection self.dragStartSelection = self.selection self.runSingleClickCommand() @@ -155,7 +181,7 @@ proc renderLine*( self.app.tryActivateEditor(self) self.markDirty() elif btn == DoubleClick: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, if isInlay: vec2() else: pos) self.selection = (line.index, offset).toSelection self.dragStartSelection = self.selection self.runDoubleClickCommand() @@ -163,7 +189,7 @@ proc renderLine*( self.app.tryActivateEditor(self) self.markDirty() elif btn == TripleClick: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, if isInlay: vec2() else: pos) self.selection = (line.index, offset).toSelection self.dragStartSelection = self.selection self.runTripleClickCommand() @@ -173,7 +199,7 @@ proc renderLine*( onDrag Left: if self.active: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, if isInlay: vec2() else: pos) let currentSelection = self.dragStartSelection let newCursor = (line.index, offset) let first = if (currentSelection.isBackwards and newCursor <= currentSelection.first) or (not currentSelection.isBackwards and newCursor >= currentSelection.first): @@ -187,12 +213,12 @@ proc renderLine*( self.markDirty() onBeginHover: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, pos) self.lastHoverLocationBounds = partNode.boundsAbsolute.some self.showHoverForDelayed (line.index, offset) onHover: - let offset = self.getCursorPos(textRuneLen, line.index, startRune.RuneIndex, pos) + let offset = self.getCursorPos(textRuneLen, line.index, startRune, pos) self.lastHoverLocationBounds = partNode.boundsAbsolute.some self.showHoverForDelayed (line.index, offset) @@ -201,7 +227,7 @@ proc renderLine*( if addBackgroundAsChildren: # Add separate background colors for selections/highlights - while colorIndex <= backgroundColors.high and backgroundColors[colorIndex].first < startRune.RuneIndex + partRuneLen: + while colorIndex <= backgroundColors.high and backgroundColors[colorIndex].first < startRune + partRuneLen: let x = max(0.0, backgroundColors[colorIndex].first.float - startRune.float) * builder.charWidth let xw = min(partRuneLen.float, backgroundColors[colorIndex].last.float - startRune.float) * builder.charWidth if backgroundColor != backgroundColors[colorIndex].color: @@ -216,19 +242,21 @@ proc renderLine*( for curs in cursors: let selectionLastRune = lineOriginal.runeIndex(curs) - if selectionLastRune >= startRune.RuneIndex and selectionLastRune < startRune.RuneIndex + partRuneLen: - let cursorX = builder.textWidth(int(selectionLastRune - startRune)).round - result.cursors.add (currentNode, $part.text[selectionLastRune - startRune], rect(cursorX, 0, builder.charWidth, builder.textHeight), (line.index, curs)) + if part.textRange.isSome: + if selectionLastRune >= part.textRange.get.startIndex and selectionLastRune < part.textRange.get.endIndex: + let cursorX = builder.textWidth(int(selectionLastRune - part.textRange.get.startIndex.RuneCount)).round + result.cursors.add (currentNode, $part.text[selectionLastRune - part.textRange.get.startIndex.RuneCount], rect(cursorX, 0, builder.charWidth, builder.textHeight), (line.index, curs)) # Set hover info if the hover location is within this part - if line.index == self.hoverLocation.line: + if line.index == self.hoverLocation.line and part.textRange.isSome: + let startRune = part.textRange.get.startIndex + let endRune = part.textRange.get.endIndex let hoverRune = lineOriginal.runeIndex(self.hoverLocation.column) - if hoverRune >= startRune.RuneIndex and hoverRune < startRune.RuneIndex + partRuneLen: + if hoverRune >= startRune and hoverRune < endRune: result.hover = (currentNode, "", rect(0, 0, builder.charWidth, builder.textHeight), self.hoverLocation).some lastPartXW = partNode.bounds.xw start += part.text.len - startRune += partRuneLen partIndex += 1 subLinePartIndex += 1 @@ -377,11 +405,7 @@ proc createTextLines(self: TextDocumentEditor, builder: UINodeBuilder, app: App, var hoverInfo = CursorLocationInfo.none proc handleScroll(delta: float) = - if self.disableScrolling: - return - let scrollAmount = delta * app.asConfigProvider.getValue("text.scroll-speed", 40.0) - self.scrollOffset += scrollAmount - self.markDirty() + self.scrollText(delta * app.asConfigProvider.getValue("text.scroll-speed", 40.0)) proc handleLine(i: int, y: float, down: bool) = let styledLine = self.getStyledText i