1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
|
# vim: ts=4:sw=4:expandtab
#
# This file is part of ReText
# Copyright: 2014, 2017 Maurice van der Pot
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from markups import MarkdownMarkup, ReStructuredTextMarkup
from PyQt6.QtGui import QTextCursor
LARGER_THAN_ANYTHING = sys.maxsize
class Row:
def __init__(self, block=None, text=None, separatorline=False, paddingchar=' '):
self.block = block
self.text = text
self.separatorline = separatorline
self.paddingchar = paddingchar
def __repr__(self):
return f"<Row '{self.text}' {self.separatorline} '{self.paddingchar}'>"
def _getTableLines(doc, pos, markupClass):
startblock = doc.findBlock(pos)
editedlineindex = 0
offset = pos - startblock.position()
rows = [ Row(block = startblock,
text = startblock.text()) ]
block = startblock.previous()
while any(c in block.text() for c in '+|'):
rows.insert(0, Row(block = block,
text = block.text()))
editedlineindex += 1
block = block.previous()
block = startblock.next()
while any(c in block.text() for c in '+|'):
rows.append(Row(block = block,
text = block.text()))
block = block.next()
if markupClass == MarkdownMarkup:
for i, row in enumerate(rows):
if i == 1:
row.separatorline = True
row.paddingchar = '-'
elif markupClass == ReStructuredTextMarkup:
for row in rows:
if row.text.strip().startswith(('+-','+=')):
row.separatorline = True
row.paddingchar = row.text.strip()[1]
row.text = row.text.replace('+', '|')
return rows, editedlineindex, offset
# Modify the edited line to put the table borders after the edition in their original positions.
# It does not matter that this function changes the position of table borders before the edition,
# because table editing mode only ever changes the table to the right of the cursor position.
def _sortaUndoEdit(rows, editedlineindex, offset, editsize):
aftertext = rows[editedlineindex].text
if editsize < 0:
beforetext = ' ' * -editsize + aftertext
else:
beforetext = aftertext[:offset] + aftertext[offset + editsize:]
rows[editedlineindex].text = beforetext
# Given text and the position of the n-th edge, returns n - 1
def _getEdgeIndex(text, edge):
return text[:edge].count('|')
def _determineRoomInCell(row, edge, edgeIndex, shrinking, startposition=0):
if len(row.text) > edge and row.text[edge] == '|' and \
(not edgeIndex or _getEdgeIndex(row.text, edge) == edgeIndex):
clearance = 0
cellwidth = 0
afterContent = True
for i in range(edge - 1, startposition - 1, -1):
if row.text[i] == '|':
break
else:
if row.text[i] == row.paddingchar and afterContent:
clearance += 1
else:
afterContent = False
cellwidth += 1
if row.separatorline:
if shrinking:
# do not shrink separator cells below 3
room = max(0, cellwidth - 3)
else:
# start expanding the cell if only the space for a right-align marker is left
room = max(0, cellwidth - 1)
else:
room = clearance
else:
room = LARGER_THAN_ANYTHING
return room
# Add an edit for a row to match the specified shift if it has an edge on the
# specified position
def _performShift(row, rowShift, edge, edgeIndex, shift):
editlist = []
# Any row that has an edge on the specified position and that doesn't
# already have edits that shift it 'shift' positions, will get an
# additional edit
if len(row.text) > edge and row.text[edge] == '|' and rowShift != shift and \
(not edgeIndex or _getEdgeIndex(row.text, edge) == edgeIndex):
editsize = -(rowShift - shift)
rowShift = shift
# Insert one position further to the left on separator lines, because
# there may be a space (for esthetical reasons) or an alignment marker
# on the last position before the edge and that should stay next to the
# edge.
if row.separatorline:
edge -= 1
editlist.append((edge, editsize))
return editlist, rowShift
# Finds the next edge position starting at offset in any row that is shifting.
# Rows that are not shifting when we are searching for an edge starting at
# offset, are rows that (upto offset) did not have any edges that aligned with
# shifting edges on other rows.
def _determineNextEdge(rows, rowShifts, offset):
nextedge = None
nextedgerow = None
for row, rowShift in zip(rows, rowShifts):
if rowShift != 0:
edge = row.text.find('|', offset)
if edge != -1 and (nextedge is None or edge < nextedge):
nextedge = edge
nextedgerow = row
return nextedge, _getEdgeIndex(nextedgerow.text, nextedge) if nextedge else None
# Return a list of edits to be made in other lines to adapt the table lines to
# a single edit in the edited line.
def _determineEditLists(rows, editedlineindex, offset, editsize, alignWithAnyEdge):
# rowShift represents how much the characters on a line will shift as a
# result of the already collected edits to be made.
rowShifts = [0 for _ in rows]
rowShifts[editedlineindex] = editsize
editLists = [[] for _ in rows]
# Find the next edge position on the edited row
currentedge, currentedgeindex = _determineNextEdge(rows, rowShifts, offset)
firstEdge = True
while currentedge:
if alignWithAnyEdge:
# Ignore what column the edge belongs to
currentedgeindex = None
if editsize < 0:
# How much an edge shifts to the left depends on how much room
# there is in the cells on any row that shares this edge.
leastLeftShift = min(
-rowShift + _determineRoomInCell(row, currentedge, currentedgeindex, True)
for row, rowShift in zip(rows, rowShifts)
)
shift = max(editsize, -leastLeftShift)
else:
# When shifting right, determine how much only once based on how
# much the edited cell needs to expand
if firstEdge:
room = _determineRoomInCell(
rows[editedlineindex],
currentedge,
currentedgeindex,
False,
offset,
)
shift = max(0, editsize - room)
for i, row in enumerate(rows):
editList, newRowShift = _performShift(
row,
rowShifts[i],
currentedge,
currentedgeindex,
shift,
)
rowShifts[i] = newRowShift
editLists[i].extend(editList)
currentedge, currentedgeindex = _determineNextEdge(rows, rowShifts, currentedge + 1)
firstEdge = False
return editLists
def _performEdits(cursor, rows, editLists, linewithoffset, offset):
cursor.joinPreviousEditBlock()
for i, (row, editList) in enumerate(zip(rows, editLists)):
for editpos, editsize in sorted(editList, reverse=True):
if i == linewithoffset:
editpos += offset
cursor.setPosition(row.block.position() + editpos)
if editsize > 0:
cursor.insertText(editsize * row.paddingchar)
else:
for _ in range(-editsize):
cursor.deletePreviousChar()
cursor.endEditBlock()
def adjustTableToChanges(doc, pos, editsize, markupClass):
if markupClass in (MarkdownMarkup, ReStructuredTextMarkup):
# This is needed because in ReSt cells can span multiple columns
# and we can therefore not determine which edges in other rows
# are supposed to be aligned with the edges in the edited row.
alignWithAnyEdge = (markupClass == ReStructuredTextMarkup)
rows, editedlineindex, offset = _getTableLines(doc, pos, markupClass)
_sortaUndoEdit(rows, editedlineindex, offset, editsize)
editLists = _determineEditLists(rows, editedlineindex, offset, editsize, alignWithAnyEdge)
cursor = QTextCursor(doc)
_performEdits(cursor, rows, editLists, editedlineindex, editsize)
def handleReturn(cursor, markupClass, newRow):
if markupClass not in (MarkdownMarkup, ReStructuredTextMarkup):
return False
positionInBlock = cursor.positionInBlock()
cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
oldLine = cursor.selectedText().lstrip('\u2029')
if not ('| ' in oldLine or ' |' in oldLine):
cursor.setPosition(cursor.block().position() + positionInBlock)
return False
indent = 0
while oldLine[indent] in ' \t':
indent += 1
indentChars, oldLine = oldLine[:indent], oldLine[indent:]
newLine = ''.join('|' if c in '+|' else ' ' for c in oldLine).rstrip()
cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock)
if newRow and markupClass == MarkdownMarkup:
sepLine = ''.join(c if c in ' |' else '-' for c in oldLine)
cursor.insertText('\n' + indentChars + sepLine)
elif newRow:
sepLine = ''.join('+' if c in '+|' else '-' for c in oldLine)
cursor.insertText('\n' + indentChars + sepLine)
cursor.insertText('\n' + indentChars + newLine)
positionInBlock = min(positionInBlock, len(indentChars + newLine))
cursor.setPosition(cursor.block().position() + positionInBlock)
return True
|