diff --git a/scripting/absytree_internal.nim b/scripting/absytree_internal.nim index 88128ddf..4ca64f7d 100644 --- a/scripting/absytree_internal.nim +++ b/scripting/absytree_internal.nim @@ -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) = diff --git a/scripting/absytree_internal_wasm.nim b/scripting/absytree_internal_wasm.nim index 1c9b2f73..59269ea5 100644 --- a/scripting/absytree_internal_wasm.nim +++ b/scripting/absytree_internal_wasm.nim @@ -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( diff --git a/scripting/editor_text_api.nim b/scripting/editor_text_api.nim index 1d04c9fe..4e88b19f 100644 --- a/scripting/editor_text_api.nim +++ b/scripting/editor_text_api.nim @@ -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, diff --git a/scripting/editor_text_api_wasm.nim b/scripting/editor_text_api_wasm.nim index fb0b6bd4..0223a55a 100644 --- a/scripting/editor_text_api_wasm.nim +++ b/scripting/editor_text_api_wasm.nim @@ -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) diff --git a/src/text/text_document.nim b/src/text/text_document.nim index a80fd827..8b953839 100644 --- a/src/text/text_document.nim +++ b/src/text/text_document.nim @@ -19,6 +19,7 @@ type Nested UndoOp = ref object oldSelection: seq[Selection] + checkpoints: seq[string] case kind: UndoOpKind of Delete: selection: Selection @@ -73,6 +74,7 @@ type TextDocument* = ref object of Document undoOps*: seq[UndoOp] redoOps*: seq[UndoOp] + nextCheckpoints: seq[string] tsParser: TSParser tsLanguage: TSLanguage @@ -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: @@ -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 @@ -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.. 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: @@ -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] @@ -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: @@ -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) @@ -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) @@ -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) @@ -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: diff --git a/src/text/text_editor.nim b/src/text/text_editor.nim index 61ae1706..649a99e0 100644 --- a/src/text/text_editor.nim +++ b/src/text/text_editor.nim @@ -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: