# 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, *, text:TTkString=" ") -> None:
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 = 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)
[docs]
def redo(self): pass
[docs]
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)
[docs]
def undo(self): pass
[docs]
def changed(self):
return self._modified
[docs]
def setChanged(self, c):
self._modified = c
if c and self._snap:
self._snap._nextDiff = None
[docs]
def lineCount(self):
return len(self._dataLines)
[docs]
def characterCount(self):
return sum([len[x] for x in self._dataLines])+self.lineCount()
[docs]
def setText(self, text):
remLines = len(self._dataLines)
if not isinstance(text, str) and not isinstance(text,TTkString):
text=str(text)
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
[docs]
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
[docs]
def isUndoAvailable(self):
return self._snap and self._snap._prevDiff
[docs]
def isRedoAvailable(self):
return self._snap and self._snap._nextDiff
[docs]
def hasSnapshots(self):
return self._snap is not None
[docs]
def snapshootId(self):
return self._snap._id
[docs]
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
[docs]
def restoreSnapshotPrev(self):
return self._restoreSnapshotDiff(False)
[docs]
def restoreSnapshotNext(self):
return self._restoreSnapshotDiff(True)
# def toHtml(self, encoding): pass
# def toMarkdown(self, features): pass
[docs]
def toAnsi(self):
return "\n".join([l.toAnsi() for l in self._dataLines])
[docs]
def toPlainText(self):
return "\n".join([str(l) for l in self._dataLines])
[docs]
def toRawText(self):
return TTkString("\n").join(self._dataLines)