Skip to content

Commit

Permalink
added undo checkpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Nimaoth committed Feb 17, 2024
1 parent 4b64c29 commit c1dfd7d
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 30 deletions.
9 changes: 7 additions & 2 deletions scripting/absytree_internal.nim
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ proc editor_text_indent_void_TextDocumentEditor_impl*(self: TextDocumentEditor)
discard
proc editor_text_unindent_void_TextDocumentEditor_impl*(self: TextDocumentEditor) =
discard
proc editor_text_undo_void_TextDocumentEditor_impl*(self: TextDocumentEditor) =
proc editor_text_undo_void_TextDocumentEditor_string_impl*(
self: TextDocumentEditor; checkpoint: string = "word") =
discard
proc editor_text_redo_void_TextDocumentEditor_impl*(self: TextDocumentEditor) =
proc editor_text_redo_void_TextDocumentEditor_string_impl*(
self: TextDocumentEditor; checkpoint: string = "word") =
discard
proc editor_text_addNextCheckpoint_void_TextDocumentEditor_string_impl*(
self: TextDocumentEditor; checkpoint: string) =
discard
proc editor_text_copy_void_TextDocumentEditor_string_bool_impl*(
self: TextDocumentEditor; register: string = ""; inclusiveEnd: bool = false) =
Expand Down
8 changes: 6 additions & 2 deletions scripting/absytree_internal_wasm.nim
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ proc editor_text_insertText_void_TextDocumentEditor_string_bool_impl(
self: TextDocumentEditor; text: string; autoIndent: bool = true) {.importc.}
proc editor_text_indent_void_TextDocumentEditor_impl(self: TextDocumentEditor) {.importc.}
proc editor_text_unindent_void_TextDocumentEditor_impl(self: TextDocumentEditor) {.importc.}
proc editor_text_undo_void_TextDocumentEditor_impl(self: TextDocumentEditor) {.importc.}
proc editor_text_redo_void_TextDocumentEditor_impl(self: TextDocumentEditor) {.importc.}
proc editor_text_undo_void_TextDocumentEditor_string_impl(
self: TextDocumentEditor; checkpoint: string = "word") {.importc.}
proc editor_text_redo_void_TextDocumentEditor_string_impl(
self: TextDocumentEditor; checkpoint: string = "word") {.importc.}
proc editor_text_addNextCheckpoint_void_TextDocumentEditor_string_impl(
self: TextDocumentEditor; checkpoint: string) {.importc.}
proc editor_text_copy_void_TextDocumentEditor_string_bool_impl(
self: TextDocumentEditor; register: string = ""; inclusiveEnd: bool = false) {.importc.}
proc editor_text_paste_void_TextDocumentEditor_string_impl(
Expand Down
11 changes: 7 additions & 4 deletions scripting/editor_text_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ proc indent*(self: TextDocumentEditor) =
editor_text_indent_void_TextDocumentEditor_impl(self)
proc unindent*(self: TextDocumentEditor) =
editor_text_unindent_void_TextDocumentEditor_impl(self)
proc undo*(self: TextDocumentEditor) =
editor_text_undo_void_TextDocumentEditor_impl(self)
proc redo*(self: TextDocumentEditor) =
editor_text_redo_void_TextDocumentEditor_impl(self)
proc undo*(self: TextDocumentEditor; checkpoint: string = "word") =
editor_text_undo_void_TextDocumentEditor_string_impl(self, checkpoint)
proc redo*(self: TextDocumentEditor; checkpoint: string = "word") =
editor_text_redo_void_TextDocumentEditor_string_impl(self, checkpoint)
proc addNextCheckpoint*(self: TextDocumentEditor; checkpoint: string) =
editor_text_addNextCheckpoint_void_TextDocumentEditor_string_impl(self,
checkpoint)
proc copy*(self: TextDocumentEditor; register: string = "";
inclusiveEnd: bool = false) =
editor_text_copy_void_TextDocumentEditor_string_bool_impl(self, register,
Expand Down
41 changes: 35 additions & 6 deletions scripting/editor_text_api_wasm.nim
Original file line number Diff line number Diff line change
Expand Up @@ -526,31 +526,60 @@ proc unindent*(self: TextDocumentEditor) =
argsJsonString.cstring)


proc editor_text_undo_void_TextDocumentEditor_wasm(arg: cstring): cstring {.
proc editor_text_undo_void_TextDocumentEditor_string_wasm(arg: cstring): cstring {.
importc.}
proc undo*(self: TextDocumentEditor) =
proc undo*(self: TextDocumentEditor; checkpoint: string = "word") =
var argsJson = newJArray()
argsJson.add block:
when TextDocumentEditor is JsonNode:
self
else:
self.toJson()
argsJson.add block:
when string is JsonNode:
checkpoint
else:
checkpoint.toJson()
let argsJsonString = $argsJson
let res {.used.} = editor_text_undo_void_TextDocumentEditor_wasm(
let res {.used.} = editor_text_undo_void_TextDocumentEditor_string_wasm(
argsJsonString.cstring)


proc editor_text_redo_void_TextDocumentEditor_wasm(arg: cstring): cstring {.
proc editor_text_redo_void_TextDocumentEditor_string_wasm(arg: cstring): cstring {.
importc.}
proc redo*(self: TextDocumentEditor) =
proc redo*(self: TextDocumentEditor; checkpoint: string = "word") =
var argsJson = newJArray()
argsJson.add block:
when TextDocumentEditor is JsonNode:
self
else:
self.toJson()
argsJson.add block:
when string is JsonNode:
checkpoint
else:
checkpoint.toJson()
let argsJsonString = $argsJson
let res {.used.} = editor_text_redo_void_TextDocumentEditor_string_wasm(
argsJsonString.cstring)


proc editor_text_addNextCheckpoint_void_TextDocumentEditor_string_wasm(
arg: cstring): cstring {.importc.}
proc addNextCheckpoint*(self: TextDocumentEditor; checkpoint: string) =
var argsJson = newJArray()
argsJson.add block:
when TextDocumentEditor is JsonNode:
self
else:
self.toJson()
argsJson.add block:
when string is JsonNode:
checkpoint
else:
checkpoint.toJson()
let argsJsonString = $argsJson
let res {.used.} = editor_text_redo_void_TextDocumentEditor_wasm(
let res {.used.} = editor_text_addNextCheckpoint_void_TextDocumentEditor_string_wasm(
argsJsonString.cstring)


Expand Down
116 changes: 104 additions & 12 deletions src/text/text_document.nim
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type
Nested
UndoOp = ref object
oldSelection: seq[Selection]
checkpoints: seq[string]
case kind: UndoOpKind
of Delete:
selection: Selection
Expand Down Expand Up @@ -73,6 +74,7 @@ type TextDocument* = ref object of Document

undoOps*: seq[UndoOp]
redoOps*: seq[UndoOp]
nextCheckpoints: seq[string]

tsParser: TSParser
tsLanguage: TSLanguage
Expand Down Expand Up @@ -248,6 +250,13 @@ func charAt*(self: TextDocument, cursor: Cursor): char =
return 0.char
return self.lines[cursor.line][cursor.column]

func runeAt*(self: TextDocument, cursor: Cursor): Rune =
if cursor.line < 0 or cursor.line > self.lines.high:
return 0.Rune
if cursor.column < 0 or cursor.column > self.lines[cursor.line].high:
return 0.Rune
return self.lines[cursor.line].runeAt(cursor.column)

func len*(line: StyledLine): int =
result = 0
for p in line.parts:
Expand Down Expand Up @@ -703,9 +712,12 @@ proc delete*(self: TextDocument, selections: openArray[Selection], oldSelection:
self.notifyTextChanged()

if record and undoOp.children.len > 0:
undoOp.checkpoints.add self.nextCheckpoints
self.undoOps.add undoOp
self.redoOps = @[]

self.nextCheckpoints = @[]

proc getNodeRange*(self: TextDocument, selection: Selection, parentIndex: int = 0, siblingIndex: int = 0): Option[Selection] =
result = Selection.none
let tree = self.tsTree
Expand Down Expand Up @@ -734,6 +746,44 @@ proc getNodeRange*(self: TextDocument, selection: Selection, parentIndex: int =

result = node.getRange.toSelection.some

proc moveCursorColumn(self: TextDocument, cursor: Cursor, offset: int, wrap: bool = true): Cursor =
var cursor = cursor
var column = cursor.column

template currentLine: openArray[char] = self.lines[cursor.line].toOpenArray

if offset > 0:
for i in 0..<offset:
if column == currentLine.len:
if not wrap:
break
if cursor.line < self.lines.high:
cursor.line = cursor.line + 1
cursor.column = 0
continue
else:
cursor.column = currentLine.len
break

cursor.column = currentLine.nextRuneStart(cursor.column)

elif offset < 0:
for i in 0..<(-offset):
if column == 0:
if not wrap:
break
if cursor.line > 0:
cursor.line = cursor.line - 1
cursor.column = currentLine.len
continue
else:
cursor.column = 0
break

cursor.column = currentLine.runeStart(cursor.column - 1)

return self.clampCursor cursor

proc firstNonWhitespace*(str: string): int =
result = 0
for c in str:
Expand Down Expand Up @@ -788,6 +838,18 @@ proc insert*(self: TextDocument, selections: openArray[Selection], oldSelection:

var undoOp = UndoOp(kind: Nested, children: @[], oldSelection: @oldSelection)

var startNewCheckpoint = false
var checkInsertedTextForCheckpoint = false

if self.undoOps.len == 0:
startNewCheckpoint = true
elif self.undoOps.last.kind == Insert:
startNewCheckpoint = true
elif self.undoOps.last.kind == Nested and self.undoOps.last.children.len != result.len:
startNewCheckpoint = true
elif self.undoOps.last.kind in {Nested, Delete}:
checkInsertedTextForCheckpoint = true

for i, selection in result:
let text = if texts.len == 1:
texts[0]
Expand Down Expand Up @@ -843,6 +905,21 @@ proc insert*(self: TextDocument, selections: openArray[Selection], oldSelection:
inc self.version

if record:
if checkInsertedTextForCheckpoint:
# echo fmt"kind: {self.undoOps.last.kind}, len: {self.undoOps.last.children.len} == {result.len}, child kind: {self.undoOps.last.children[i].kind}, cursor {self.undoOps.last.children[i].selection.last} != {oldCursor}"
if text.len > 1:
startNewCheckpoint = true
elif self.undoOps.last.kind == Nested and self.undoOps.last.children.len == result.len and self.undoOps.last.children[i].kind == Delete:
if self.undoOps.last.children[i].selection.last != oldCursor:
startNewCheckpoint = true
else:
let lastInsertedChar = if oldCursor.column == 0: '\n' else: self.charAt(self.moveCursorColumn(oldCursor, -1))
let newInsertedChar = text[0]
# echo fmt"last: '{lastInsertedChar}', new: '{newInsertedChar}'"

if lastInsertedChar notin Whitespace and newInsertedChar in Whitespace or lastInsertedChar == '\n':
startNewCheckpoint = true

undoOp.children.add UndoOp(kind: Delete, selection: (oldCursor, cursor))

if notify:
Expand All @@ -852,9 +929,15 @@ proc insert*(self: TextDocument, selections: openArray[Selection], oldSelection:
self.notifyTextChanged()

if record and undoOp.children.len > 0:
if startNewCheckpoint:
undoOp.checkpoints.add "word"
undoOp.checkpoints.add self.nextCheckpoints

self.undoOps.add undoOp
self.redoOps = @[]

self.nextCheckpoints = @[]

proc edit*(self: TextDocument, selections: openArray[Selection], oldSelection: openArray[Selection], texts: openArray[string], notify: bool = true, record: bool = true): seq[Selection] =
let selections = selections.map (s) => s.normalized
result = self.delete(selections, oldSelection, false, record=record)
Expand All @@ -865,17 +948,17 @@ proc doUndo(self: TextDocument, op: UndoOp, oldSelection: openArray[Selection],
of Delete:
let text = self.contentString(op.selection)
result = self.delete([op.selection], op.oldSelection, record=false)
redoOps.add UndoOp(kind: Insert, cursor: op.selection.first, text: text, oldSelection: @oldSelection)
redoOps.add UndoOp(kind: Insert, cursor: op.selection.first, text: text, oldSelection: @oldSelection, checkpoints: op.checkpoints)

of Insert:
let selections = self.insert([op.cursor.toSelection], op.oldSelection, [op.text], record=false)
result = selections
redoOps.add UndoOp(kind: Delete, selection: (op.cursor, selections[0].last), oldSelection: @oldSelection)
redoOps.add UndoOp(kind: Delete, selection: (op.cursor, selections[0].last), oldSelection: @oldSelection, checkpoints: op.checkpoints)

of Nested:
result = op.oldSelection

var redoOp = UndoOp(kind: Nested, oldSelection: @oldSelection)
var redoOp = UndoOp(kind: Nested, oldSelection: @oldSelection, checkpoints: op.checkpoints)
for i in countdown(op.children.high, 0):
discard self.doUndo(op.children[i], oldSelection, useOldSelection, redoOp.children)

Expand All @@ -884,30 +967,33 @@ proc doUndo(self: TextDocument, op: UndoOp, oldSelection: openArray[Selection],
if useOldSelection:
result = op.oldSelection

proc undo*(self: TextDocument, oldSelection: openArray[Selection], useOldSelection: bool): Option[seq[Selection]] =
proc undo*(self: TextDocument, oldSelection: openArray[Selection], useOldSelection: bool, untilCheckpoint: string = ""): Option[seq[Selection]] =
result = seq[Selection].none

if self.undoOps.len == 0:
return

let op = self.undoOps.pop
return self.doUndo(op, oldSelection, useOldSelection, self.redoOps).some
while self.undoOps.len > 0:
let op = self.undoOps.pop
result = self.doUndo(op, oldSelection, useOldSelection, self.redoOps).some
if untilCheckpoint.len == 0 or untilCheckpoint in op.checkpoints:
break

proc doRedo(self: TextDocument, op: UndoOp, oldSelection: openArray[Selection], useOldSelection: bool, undoOps: var seq[UndoOp]): seq[Selection] =
case op.kind:
of Delete:
let text = self.contentString(op.selection)
result = self.delete([op.selection], op.oldSelection, record=false)
undoOps.add UndoOp(kind: Insert, cursor: op.selection.first, text: text, oldSelection: @oldSelection)
undoOps.add UndoOp(kind: Insert, cursor: op.selection.first, text: text, oldSelection: @oldSelection, checkpoints: op.checkpoints)

of Insert:
result = self.insert([op.cursor.toSelection], [op.cursor.toSelection], [op.text], record=false)
undoOps.add UndoOp(kind: Delete, selection: (op.cursor, result[0].last), oldSelection: @oldSelection)
undoOps.add UndoOp(kind: Delete, selection: (op.cursor, result[0].last), oldSelection: @oldSelection, checkpoints: op.checkpoints)

of Nested:
result = op.oldSelection

var undoOp = UndoOp(kind: Nested, oldSelection: @oldSelection)
var undoOp = UndoOp(kind: Nested, oldSelection: @oldSelection, checkpoints: op.checkpoints)
for i in countdown(op.children.high, 0):
discard self.doRedo(op.children[i], oldSelection, useOldSelection, undoOp.children)

Expand All @@ -916,14 +1002,20 @@ proc doRedo(self: TextDocument, op: UndoOp, oldSelection: openArray[Selection],
if useOldSelection:
result = op.oldSelection

proc redo*(self: TextDocument, oldSelection: openArray[Selection], useOldSelection: bool): Option[seq[Selection]] =
proc redo*(self: TextDocument, oldSelection: openArray[Selection], useOldSelection: bool, untilCheckpoint: string = ""): Option[seq[Selection]] =
result = seq[Selection].none

if self.redoOps.len == 0:
return

let op = self.redoOps.pop
return self.doRedo(op, oldSelection, useOldSelection, self.undoOps).some
while self.redoOps.len > 0:
let op = self.redoOps.pop
result = self.doRedo(op, oldSelection, useOldSelection, self.undoOps).some
if untilCheckpoint.len == 0 or (self.redoOps.len > 0 and untilCheckpoint in self.redoOps.last.checkpoints):
break

proc addNextCheckpoint*(self: TextDocument, checkpoint: string) =
self.nextCheckpoints.add checkpoint

proc isLineEmptyOrWhitespace*(self: TextDocument, line: int): bool =
if line > self.lines.high:
Expand Down
11 changes: 7 additions & 4 deletions src/text/text_editor.nim
Original file line number Diff line number Diff line change
Expand Up @@ -728,16 +728,19 @@ proc unindent*(self: TextDocumentEditor) {.expose("editor.text").} =
s.last.column = max(0, s.last.column - self.document.indentStyle.indentColumns)
self.selections = selections

proc undo*(self: TextDocumentEditor) {.expose("editor.text").} =
if self.document.undo(self.selections, true).getSome(selections):
proc undo*(self: TextDocumentEditor, checkpoint: string = "word") {.expose("editor.text").} =
if self.document.undo(self.selections, true, checkpoint).getSome(selections):
self.selections = selections
self.scrollToCursor(Last)

proc redo*(self: TextDocumentEditor) {.expose("editor.text").} =
if self.document.redo(self.selections, true).getSome(selections):
proc redo*(self: TextDocumentEditor, checkpoint: string = "word") {.expose("editor.text").} =
if self.document.redo(self.selections, true, checkpoint).getSome(selections):
self.selections = selections
self.scrollToCursor(Last)

proc addNextCheckpoint*(self: TextDocumentEditor, checkpoint: string) {.expose("editor.text").} =
self.document.addNextCheckpoint checkpoint

proc copyAsync*(self: TextDocumentEditor, register: string, inclusiveEnd: bool): Future[void] {.async.} =
var text = ""
for i, selection in self.selections:
Expand Down

0 comments on commit c1dfd7d

Please sign in to comment.