Source code for TermTk.TTkGui.textdocument

# MIT License
#
# Copyright (c) 2022 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TTkTextDocument']

from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkCore.string import TTkString

[docs]class TTkTextDocument(): ''' Undo,Redo Logic Old: _snapshotId: = last saved/undo/redo state 3 = doc4 _snapshots: [doc1, doc2, doc3, doc4, doc5, doc6, . . .] New: SnapshotId: 2 Snapshots: _lastSnap _dataLines (unstaged) ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ Cursors: c0, c1, c2, c3, c4 = _lastCursor Diffs: [ d01, d12, d23, d34 ] = Forward Diffs [ d10, d21, d32, d43 ] = Backward Diffs Slices: = common txt slices between snapshots [ s01, s12, s23, s34 ] Data Structure ╔═══════════════╗ ╔═══════════════╗ ║ Snapshot B ║ ┌─────────────>║ Snapshot C ║ ╟───────────────╢ │ ╟───────────────╢ ║ _nextDiff ║──────┐ │ ║ _nextDiff ║───> Next snapshot ┌───║ _prevDiff ║ │ │ ┌───║ _prevDiff ║ or Null if at the end │ ╚═══════════════╝ │ │ │ ╚═══════════════╝ V A V │ V ╔═══════════════╗ │ ╔═══════════════╗ ╔═══════════════╗ ║ Diff B->A ║ │ ║ Diff B->C ║ ║ Diff C->B ║ ╟───────────────╢ │ ╟───────────────╢ ╟───────────────╢ ║ slice = txtBA ║ │ ║ slice = txtBC ║ ║ slice = txtBA ║ ║ snap ║ │ ║ snap ║ ║ snap ║ ╚═══════════════╝ │ ╚═══════════════╝ ╚═══════════════╝ │ │ └─────────────────────────────┘ ''' class _snapDiff(): ''' Doc: 0 i1 12 Base: |---------aaaaaaaa---------| Mod: |---------bbbbb ---------| 0 slice ''' __slots__ = ('_slice', '_i1', '_i2', '_snap') def __init__(self, txt, i1, i2, snap): # The text slice required to change the current snap to the next one self._slice = txt # Starting position of the slice to be removed self._i1 = i1 # Ending position of the slice to be removed self._i2 = i2 # This is the link to the next _snapshot structure self._snap = snap class _snapshot(): _lastId = 0 __slots__ = ( '_cursor', '_id', '_nextDiff', '_prevDiff') def __init__(self, cursor, nextDiff, prevDiff): self._cursor = cursor self._nextDiff = nextDiff self._prevDiff = prevDiff self._id = TTkTextDocument._snapshot._lastId = self._lastId+1 # TTkLog.debug(f"{self._id=}") def getNextSnap(self, lines): return self._getSnap(lines, self._nextDiff) def getPrevSnap(self, lines): return self._getSnap(lines, self._prevDiff) def _getSnap(self, lines, d): lines[d._i1:d._i2] = d._slice return d._snap __slots__ = ( '_dataLines', '_modified', '_snap', '_snapChanged', '_lastSnap', '_lastCursor', # Signals 'contentsChange', 'contentsChanged', 'cursorPositionChanged', 'undoAvailable', 'redoAvailable', 'undoCommandAdded', 'modificationChanged' ) def __init__(self, *args, **kwargs): from TermTk.TTkGui.textcursor import TTkTextCursor self.cursorPositionChanged = pyTTkSignal(TTkTextCursor) self.contentsChange = pyTTkSignal(int,int,int) # int line, int linesRemoved, int linesAdded self.contentsChanged = pyTTkSignal() self.undoAvailable = pyTTkSignal(bool) self.redoAvailable = pyTTkSignal(bool) self.undoCommandAdded = pyTTkSignal() self.modificationChanged = pyTTkSignal(bool) text = kwargs.get('text'," ") self._dataLines = [TTkString(t) for t in text.split('\n')] self._modified = False # Cumulative changes since the lasrt snapshot self._snapChanged = None self.contentsChange.connect(self._saveSnapChanged) self._lastSnap = self._dataLines.copy() self._lastCursor = TTkTextCursor(document=self) self._snap = TTkTextDocument._snapshot(self._lastCursor, None, None) # I need this moethod to cover the math of merging # multiples retuen values to be used in the contentsChange # method # # ┬ ┬ ┬ ┬ # x2 -│----│-----l2 ┬┼----┼┐ # x1 l1 ┬┼----┼┐ ││ ││ # ││ ││ a1 r2 ││ ││ a2 # ││ /┼┘-------││-. ││ # r1 ││ /.│--------└┼-.. ││ # ││ /. │ │ \.││-z1 # y1 └┼'. /┴ ┴-. -┼┘-z2 # y2 _│. / \ │ # │ / -┴ # ┴' # # x1 = l1 # x2 = l2 # y1 = l1+r1 # y2 = l2+r2 + (r1-a1) # z1 = l1+a1 + (a2-r2) # z2 = l2+a2 @staticmethod def _mergeChangesSlices(ch1,ch2): l1,r1,a1 = ch1 l2,r2,a2 = ch2 x1 = l1 x2 = l2 y1 = l1+r1 y2 = l2+r2 + (r1-a1) z1 = l1+a1 + (a2-r2) z2 = l2+a2 a = min(x1,x2) b = max(y1,y2) - a c = max(z1,z2) - a return a,b,c @pyTTkSlot(int,int,int) def _saveSnapChanged(self,a,b,c): if self._snapChanged: self._snapChanged = TTkTextDocument._mergeChangesSlices(self._snapChanged,(a,b,c)) else: self._snapChanged = (a,b,c) def redo(self): pass def setModified(self, m=True): if m and self._snap: self._snap._nextDiff = None if self._modified == m: return self._modified = m self.modificationChanged.emit(m) def undo(self): pass def changed(self): return self._modified def setChanged(self, c): self._modified = c if c and self._snap: self._snap._nextDiff = None def lineCount(self): return len(self._dataLines) def characterCount(self): return sum([len[x] for x in self._dataLines])+self.lineCount() def setText(self, text): remLines = len(self._dataLines) self._dataLines = [TTkString(t) for t in text.split('\n')] self._modified = False self._lastSnap = self._dataLines.copy() self._snap = TTkTextDocument._snapshot(self._lastCursor, None, None) self.contentsChanged.emit() self.contentsChange.emit(0,remLines,len(self._dataLines)) self._snapChanged = None def appendText(self, text): if type(text) == str: text = TTkString() + text oldLines = len(self._dataLines) self._dataLines += text.split('\n') self._modified = False self._lastSnap = self._dataLines.copy() self._snap = TTkTextDocument._snapshot(self._lastCursor, None, None) self.contentsChanged.emit() self.contentsChange.emit(oldLines,0,len(self._dataLines)-oldLines) self._snapChanged = None def isUndoAvailable(self): return self._snap and self._snap._prevDiff def isRedoAvailable(self): return self._snap and self._snap._nextDiff def hasSnapshots(self): return self._snap is not None def snapshootId(self): return self._snap._id def saveSnapshot(self, cursor): docA = self._lastSnap docB = self._dataLines # get the # sa = starting line # sb = removed lines # sc = added lines # of the cumulative changes applied since the last snapshot sa,sb,sc = self._snapChanged if self._snapChanged else (0,0,0) self._snapChanged = None sliceA = docA[sa:sa+sb] sliceB = docB[sa:sa+sc] if sliceA or sliceB: # current snapshot # is becoming the previous one snapA = self._snap diffBA = TTkTextDocument._snapDiff(sliceA, sa, sa+sc, snapA) snapB = TTkTextDocument._snapshot(cursor, None, diffBA) diffAB = TTkTextDocument._snapDiff(sliceB, sa, sa+sb, snapB) snapA._nextDiff = diffAB self._snap = snapB else: self._snap._cursor = cursor self._modified = False self._lastSnap = self._dataLines.copy() self._lastCursor = cursor self.undoAvailable.emit(self.isUndoAvailable()) self.redoAvailable.emit(self.isRedoAvailable()) def _restoreSnapshotDiff(self, next=True): if ( not self._snap or ( next and not self._snap._nextDiff) or (not next and not self._snap._prevDiff) ): return None if next: self._snap = self._snap.getNextSnap(self._dataLines) else: self._snap = self._snap.getPrevSnap(self._dataLines) self._lastSnap = self._dataLines.copy() self._lastCursor = self._snap._cursor.copy() self.contentsChanged.emit() self.undoAvailable.emit(self.isUndoAvailable()) self.redoAvailable.emit(self.isRedoAvailable()) return self._snap._cursor def restoreSnapshotPrev(self): return self._restoreSnapshotDiff(False) def restoreSnapshotNext(self): return self._restoreSnapshotDiff(True) # def toHtml(self, encoding): pass # def toMarkdown(self, features): pass def toAnsi(self): return "\n".join([l.toAnsi() for l in self._dataLines]) def toPlainText(self): return "\n".join([str(l) for l in self._dataLines]) def toRawText(self): return TTkString("\n").join(self._dataLines)