Skip to content

Commit

Permalink
move inlay hints when changing text
Browse files Browse the repository at this point in the history
  • Loading branch information
Nimaoth committed Mar 13, 2024
1 parent 8080f61 commit aeeaedc
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 18 deletions.
1 change: 1 addition & 0 deletions scripting/editor_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ else:
proc getBackend*(): Backend =
editor_getBackend_Backend_App_impl()
proc loadApplicationFile*(path: string): Option[string] =
## Load a file from the application directory (path is relative to the executable)
editor_loadApplicationFile_Option_string_App_string_impl(path)
proc toggleShowDrawnNodes*() =
editor_toggleShowDrawnNodes_void_App_impl()
Expand Down
5 changes: 4 additions & 1 deletion src/misc/util.nim
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,7 @@ else:
## Concatenates `x` and `y` in place.
##
## See also `system.add`.
x.add(y)
x.add(y)

func removeSwap*[T](s: var seq[T]; index: int) = s.del(index)
func removeShift*[T](s: var seq[T]; index: int) = s.delete(index)
7 changes: 7 additions & 0 deletions src/scripting_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ proc normalize*(self: var Selections) =
proc normalized*(self: Selections): Selections =
return self.sorted

proc lastLineLen*(self: Selection): int =
## Returns the length of the selection on the last line covered by the selection
if self.first.line == self.last.line:
result = self.last.column - self.first.column
else:
result = self.last.column

var nextEditorId = 0
proc newEditorId*(): EditorId =
## Returns a new unique id for an editor
Expand Down
8 changes: 4 additions & 4 deletions src/text/language/language_server_lsp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string],
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.first.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard textDocumentEditor.document.textDeleted.subscribe proc(args: auto) =
# debugf"TEXT DELETED {args.document.fullPath}: {args.selection}"
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard gEditor.onEditorDeregistered.subscribe proc(editor: auto) =
Expand Down Expand Up @@ -124,15 +124,15 @@ proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string],
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.first.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard textDocumentEditor.document.textDeleted.subscribe proc(args: auto) =
# debugf"TEXT DELETED {args.document.fullPath}: {args.selection}"
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

log lvlInfo, fmt"Started language server for {languageId}"
Expand Down
2 changes: 1 addition & 1 deletion src/text/language/lsp_client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ when not defined(js):
proc logProcessDebugOutput(process: AsyncProcess) {.async.} =
while process.isAlive:
let line = await process.recvErrorLine
log(lvlDebug, fmt"[debug] {line}")
# log(lvlDebug, fmt"[debug] {line}")

proc sendInitializationRequest(client: LSPClient) {.async.} =
log(lvlInfo, "Initializing client...")
Expand Down
6 changes: 3 additions & 3 deletions src/text/text_document.nim
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ type TextDocument* = ref object of Document

onLoaded*: Event[TextDocument]
textChanged*: Event[TextDocument]
textInserted*: Event[tuple[document: TextDocument, location: Cursor, text: string]]
textDeleted*: Event[tuple[document: TextDocument, selection: Selection]]
textInserted*: Event[tuple[document: TextDocument, location: Selection, text: string]]
textDeleted*: Event[tuple[document: TextDocument, location: Selection]]
singleLine*: bool

changes: seq[TextDocumentChange]
Expand Down Expand Up @@ -949,7 +949,7 @@ proc insert*(self: TextDocument, selections: openArray[Selection], oldSelection:
undoOp.children.add UndoOp(kind: Delete, selection: (oldCursor, cursor))

if notify:
self.textInserted.invoke((self, oldCursor, text))
self.textInserted.invoke((self, (oldCursor, cursor), text))

if notify:
self.notifyTextChanged()
Expand Down
58 changes: 57 additions & 1 deletion src/text/text_editor.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,7 @@ proc updateInlayHintsAsync*(self: TextDocumentEditor): Future[void] {.async.} =
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}"
# log lvlInfo, fmt"Updating inlay hints: {inlayHints}"
self.inlayHints = inlayHints
self.markDirty()

Expand Down Expand Up @@ -1943,6 +1943,58 @@ method injectDependencies*(self: TextDocumentEditor, app: AppInterface) =

self.setMode(self.configProvider.getValue("editor.text.default-mode", ""))

proc updateInlayHintPositionsAfterInsert(self: TextDocumentEditor, location: Selection) =
# todo: correctly handle unicode
for inlayHint in self.inlayHints.mitems:
if inlayHint.location.line == location.first.line and inlayHint.location.column > location.first.column:
# inserted text on same line before inlayHint
inlayHint.location.column += location.last.column - location.first.column
inlayHint.location.line += location.last.line - location.first.line
elif inlayHint.location.line == location.first.line and inlayHint.location.column == location.first.column:
# Inserted text at inlayHint location
# Usually the inlay hint will be on a word boundary, so if it's not anymore then move the inlay hint
# (this happens if you e.g. change the name of the variable by appending some text)
# If it's still on a word boundary, then the new inlay hint will probably be at the same location so don't move it now
let wordBoundary = self.findWordBoundary(inlayHint.location)
if inlayHint.location != wordBoundary.first and inlayHint.location != wordBoundary.last:
inlayHint.location.column += location.last.column - location.first.column
inlayHint.location.line += location.last.line - location.first.line
elif inlayHint.location.line > location.first.line:
# inserted text on line before inlay hint
inlayHint.location.line += location.last.line - location.first.line

proc updateInlayHintPositionsAfterDelete(self: TextDocumentEditor, selection: Selection) =
# todo: correctly handle unicode (inlay hint location comes from lsp, so probably a RuneIndex)
let selection = selection.normalized
for i in countdown(self.inlayHints.high, 0):
template inlayHint: InlayHint = self.inlayHints[i]

if selection.first.line == selection.last.line:
if inlayHint.location.line == selection.first.line and inlayHint.location.column >= selection.last.column:
inlayHint.location.column -= selection.last.column - selection.first.column
elif inlayHint.location.line == selection.first.line and inlayHint.location.column > selection.first.column and inlayHint.location.column < selection.last.column:
self.inlayHints.removeSwap(i)
else:
var remove = false
if inlayHint.location.line == selection.first.line and inlayHint.location.column >= selection.first.column:
remove = true
elif inlayHint.location.line > selection.first.line and inlayHint.location.line < selection.last.line:
remove = true
elif inlayHint.location.line == selection.last.line and inlayHint.location.column <= selection.last.column:
remove = true
elif inlayHint.location.line == selection.last.line and inlayHint.location.column >= selection.last.column:
inlayHint.location.column -= selection.last.column - selection.first.column
if inlayHint.location.line >= selection.last.line:
inlayHint.location.line -= selection.last.line - selection.first.line
if remove:
self.inlayHints.removeSwap(i)

proc handleTextInserted(self: TextDocumentEditor, document: TextDocument, location: Selection, text: string) =
self.updateInlayHintPositionsAfterInsert(location)

proc handleTextDeleted(self: TextDocumentEditor, document: TextDocument, selection: Selection) =
self.updateInlayHintPositionsAfterDelete(selection)

proc handleTextDocumentTextChanged(self: TextDocumentEditor) =
self.clampSelection()
self.updateSearchResults()
Expand Down Expand Up @@ -1989,6 +2041,8 @@ proc newTextEditor*(document: TextDocument, app: AppInterface, configProvider: C
self.injectDependencies(app)
discard document.textChanged.subscribe (_: TextDocument) => self.handleTextDocumentTextChanged()
discard document.onLoaded.subscribe (_: TextDocument) => self.handleTextDocumentLoaded()
discard document.textInserted.subscribe (arg: tuple[document: TextDocument, location: Selection, text: string]) => self.handleTextInserted(arg.document, arg.location, arg.text)
discard document.textDeleted.subscribe (arg: tuple[document: TextDocument, location: Selection]) => self.handleTextDeleted(arg.document, arg.location)

return self

Expand All @@ -2004,6 +2058,8 @@ method createWithDocument*(_: TextDocumentEditor, document: Document, configProv
self.document.lines = @[""]
discard self.document.textChanged.subscribe (_: TextDocument) => self.handleTextDocumentTextChanged()
discard self.document.onLoaded.subscribe (_: TextDocument) => self.handleTextDocumentLoaded()
discard self.document.textInserted.subscribe (arg: tuple[document: TextDocument, location: Selection, text: string]) => self.handleTextInserted(arg.document, arg.location, arg.text)
discard self.document.textDeleted.subscribe (arg: tuple[document: TextDocument, location: Selection]) => self.handleTextDeleted(arg.document, arg.location)

self.startBlinkCursorTask()

Expand Down
26 changes: 18 additions & 8 deletions src/ui/widget_builder_text_document.nim
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ proc renderLine*(
var partIndex = 0
var subLineIndex = 0
var subLinePartIndex = 0
var previousInlayNode: UINode = nil
while partIndex < line.parts.len: # outer loop for wrapped lines within this line

builder.panel(flagsInner + LayoutHorizontal):
Expand Down Expand Up @@ -115,8 +116,10 @@ proc renderLine*(
let textColor = if part.scope.len == 0: textColor else: theme.tokenColor(part, textColor)

var startRune = 0.RuneIndex
var endRune = 0.RuneIndex
if part.textRange.isSome:
startRune = part.textRange.get.startIndex
endRune = part.textRange.get.endIndex
else:
# Inlay text, find start rune of neighbor, prefer left side
var found = false
Expand All @@ -132,15 +135,12 @@ proc renderLine*(
startRune = line.parts[i].textRange.get.startIndex
break

endRune = startRune

# Find background color
var colorIndex = 0
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
while colorIndex < backgroundColors.high and (backgroundColors[colorIndex].first == backgroundColors[colorIndex].last or backgroundColors[colorIndex].last <= startRune):
inc colorIndex

var backgroundColor = backgroundColor
var addBackgroundAsChildren = true
Expand Down Expand Up @@ -244,8 +244,13 @@ proc renderLine*(

if part.textRange.isSome:
if selectionLastRune >= part.textRange.get.startIndex and selectionLastRune < part.textRange.get.endIndex:
let node = if selectionLastRune == startRune and previousInlayNode.isNotNil:
# show cursor on first position of previous inlay
previousInlayNode
else:
partNode
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))
result.cursors.add (node, $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 and part.textRange.isSome:
Expand All @@ -255,6 +260,11 @@ proc renderLine*(
if hoverRune >= startRune and hoverRune < endRune:
result.hover = (currentNode, "", rect(0, 0, builder.charWidth, builder.textHeight), self.hoverLocation).some

if part.textRange.isNone:
previousInlayNode = partNode
else:
previousInlayNode = nil

lastPartXW = partNode.bounds.xw
start += part.text.len
partIndex += 1
Expand Down

0 comments on commit aeeaedc

Please sign in to comment.