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