vibefpmgui.multi_cursor
Several cursors at once in the editor (⌥-click, column selection).
A part of vibefpmgui.code_editor.CodeEditor.
1"""Several cursors at once in the editor (⌥-click, column selection). 2 3A part of `vibefpmgui.code_editor.CodeEditor`.""" 4 5from __future__ import annotations 6 7import sys 8 9from qtpy.QtCore import Qt 10from qtpy.QtGui import QKeySequence, QTextCursor 11from qtpy.QtWidgets import QApplication 12 13 14class MultiCursorMixin: 15 """Several cursors at once (a part of `vibefpmgui.code_editor.CodeEditor`).""" 16 17 def all_cursors(self) -> list[QTextCursor]: 18 """Every cursor, the main one included, in the order they're in the text.""" 19 return sorted([self.textCursor(), *self.extra_cursors], key=lambda c: c.position()) 20 21 def toggle_cursor_at(self, position: int): 22 """⌥/Alt-click: add a cursor there, or remove the one that's there.""" 23 for c in self.extra_cursors: 24 if c.position() == position: 25 self.extra_cursors.remove(c) 26 self._cursors_changed() 27 return 28 if position == self.textCursor().position() and not self.textCursor().hasSelection(): 29 return # (the main cursor is already there) 30 cursor = QTextCursor(self.document()) 31 cursor.setPosition(position) 32 self.extra_cursors.append(cursor) 33 self._cursors_changed() 34 35 def add_column_cursors(self, to_line: int, to_column: int | None = None): 36 """⌘⇧/Ctrl+Shift-click: a cursor on each line from the main cursor's 37 line to ``to_line`` (0-based). Clicked in the main cursor's column: 38 cursors in that column; in another (``to_column``): a box selection, 39 each line selected from the one column to the other, its cursor at 40 the clicked end (both clipped to a shorter line's end).""" 41 main = self.textCursor() 42 first, column = main.blockNumber(), main.positionInBlock() 43 end_column = column if to_column is None else to_column 44 45 def on_line(block) -> QTextCursor: 46 last = block.length() - 1 # (the line's end, before the line break) 47 cursor = QTextCursor(block) 48 cursor.setPosition(block.position() + min(column, last)) 49 cursor.setPosition(block.position() + min(end_column, last), QTextCursor.KeepAnchor) 50 return cursor 51 52 self.setTextCursor(on_line(main.block())) 53 step = 1 if to_line >= first else -1 54 for number in range(first + step, to_line + step, step): 55 block = self.document().findBlockByNumber(number) 56 if not block.isValid(): 57 break 58 self.extra_cursors.append(on_line(block)) 59 self._cursors_changed() 60 61 def clear_extra_cursors(self): 62 if self.extra_cursors: 63 self.extra_cursors = [] 64 self._cursors_changed() 65 66 def _cursors_changed(self): 67 # Two cursors on the same spot are one (e.g. after deleting between them). 68 seen = {(self.textCursor().position(), self.textCursor().anchor())} 69 kept = [] 70 for c in self.extra_cursors: 71 key = (c.position(), c.anchor()) 72 if key not in seen: 73 seen.add(key) 74 kept.append(c) 75 self.extra_cursors = kept 76 if kept and self.cursorWidth(): # several cursors: draw them all ourselves 77 self._caret_width = self.cursorWidth() 78 self.setCursorWidth(0) 79 elif not kept and not self.cursorWidth(): # one again: Qt's own 80 self.setCursorWidth(self._caret_width) 81 self._restart_blink() 82 self.update_extra_selections(rebuild=False) # (the cursors' selections are always made anew) 83 self.viewport().update() 84 self.cursors_changed.emit() 85 86 def _restart_blink(self): 87 """Typing or moving shows the cursors and starts the blink again (as 88 Qt does with its own).""" 89 if not self.extra_cursors: 90 self._caret_timer.stop() 91 return 92 self._caret_on = True 93 flash = QApplication.cursorFlashTime() # (a whole on-off cycle; 0: no blinking) 94 if flash > 0: 95 self._caret_timer.start(flash // 2) 96 else: 97 self._caret_timer.stop() 98 self.viewport().update() 99 100 def _blink(self): 101 self._caret_on = not self._caret_on 102 self.viewport().update() 103 104 def _at_each_cursor(self, edit): 105 """``edit(cursor)`` at every cursor, as one Undo step.""" 106 main = self.textCursor() 107 cursors = [main, *self.extra_cursors] 108 main.beginEditBlock() # (the document's: every cursor's edits are in it) 109 for c in sorted(cursors, key=lambda c: c.position(), reverse=True): 110 edit(c) 111 main.endEditBlock() 112 self.setTextCursor(main) 113 self._cursors_changed() 114 115 def visual_column(self, cursor: QTextCursor) -> int: 116 """The column a position shows at (a tab: to the next tab stop).""" 117 tab = max(1, round(self.tabStopDistance() / max(1, self.fontMetrics().horizontalAdvance(" ")))) 118 column = 0 119 for ch in cursor.block().text()[:cursor.positionInBlock()]: 120 column = (column // tab + 1) * tab if ch == "\t" else column + 1 121 return column 122 123 def align_cursors(self) -> bool: 124 """Edit → Lines → Align Cursors: spaces before each cursor left of the 125 right-most one, so they're all in its column (one Undo step). Several 126 cursors on a line: the first on each line aligned, then the second, and 127 so on. A cursor with a selection: by its left edge (the selection moves 128 along). False: fewer than two cursors (nothing to align).""" 129 cursors = self.all_cursors() 130 if len(cursors) < 2: 131 return False 132 lines: dict[int, list[QTextCursor]] = {} 133 for c in cursors: 134 edge = QTextCursor(c) # (where the spaces go: a selection's left edge) 135 edge.setPosition(c.selectionStart()) 136 lines.setdefault(edge.blockNumber(), []).append(edge) 137 for row in lines.values(): 138 row.sort(key=lambda c: c.position()) 139 main = self.textCursor() 140 main.beginEditBlock() 141 for k in range(max(len(row) for row in lines.values())): 142 column_k = [row[k] for row in lines.values() if len(row) > k] 143 target = max(self.visual_column(c) for c in column_k) 144 for c in column_k: 145 gap = target - self.visual_column(c) 146 if gap > 0: 147 c.insertText(" " * gap) # (the cursors after it on the line move along) 148 main.endEditBlock() 149 self.setTextCursor(main) 150 self._cursors_changed() 151 return True 152 153 def _move_each_cursor(self, operation, select: bool): 154 mode = QTextCursor.KeepAnchor if select else QTextCursor.MoveAnchor 155 main = self.textCursor() 156 for c in (main, *self.extra_cursors): 157 c.movePosition(operation, mode) 158 self.setTextCursor(main) 159 self._cursors_changed() 160 161 def _multi_cursor_key(self, event) -> bool: 162 """A key with several cursors: True if it was handled here.""" 163 self.completion_popup.hide() 164 self.signature_popup.hide() 165 key, mods = event.key(), event.modifiers() 166 shift = bool(mods & Qt.ShiftModifier) 167 word = bool(mods & (Qt.AltModifier if sys.platform == "darwin" else Qt.ControlModifier)) 168 if key == Qt.Key_Escape: 169 self.clear_extra_cursors() 170 return True 171 if event.matches(QKeySequence.Copy): 172 self.copy() 173 return True 174 if event.matches(QKeySequence.Cut): 175 self.cut() 176 return True 177 if event.matches(QKeySequence.Paste): 178 self.paste() 179 return True 180 moves = {Qt.Key_Left: QTextCursor.WordLeft if word else QTextCursor.Left, 181 Qt.Key_Right: QTextCursor.WordRight if word else QTextCursor.Right, 182 Qt.Key_Up: QTextCursor.Up, Qt.Key_Down: QTextCursor.Down, 183 Qt.Key_Home: QTextCursor.StartOfBlock, Qt.Key_End: QTextCursor.EndOfBlock} 184 command = sys.platform == "darwin" and mods & Qt.ControlModifier 185 if command and key in (Qt.Key_Left, Qt.Key_Right): # ⌘← / ⌘→: the start / end of the line 186 self._move_each_cursor(QTextCursor.StartOfBlock if key == Qt.Key_Left else QTextCursor.EndOfBlock, 187 shift) 188 return True 189 if key in moves and not command: 190 self._move_each_cursor(moves[key], shift) 191 return True 192 if key == Qt.Key_Backspace: 193 self._at_each_cursor(lambda c: c.removeSelectedText() if c.hasSelection() else c.deletePreviousChar()) 194 return True 195 if key == Qt.Key_Delete: 196 self._at_each_cursor(lambda c: c.removeSelectedText() if c.hasSelection() else c.deleteChar()) 197 return True 198 if key in (Qt.Key_Return, Qt.Key_Enter): 199 def newline(c): 200 line = c.block().text() 201 c.insertText("\n" + line[:len(line) - len(line.lstrip())]) # (keeping the indentation) 202 self._at_each_cursor(newline) 203 return True 204 if key == Qt.Key_Tab: 205 self._at_each_cursor(lambda c: c.insertText(self.tab_text(c))) 206 return True 207 text = event.text() 208 if text and text.isprintable() and not mods & (Qt.ControlModifier | Qt.MetaModifier): 209 self._at_each_cursor(lambda c: c.insertText(text)) 210 return True 211 return False # (other keys: shortcuts and the like, for the main cursor) 212 213 def copy(self): 214 if not self.extra_cursors: 215 super().copy() 216 return 217 texts = [c.selectedText().replace("\u2029", "\n") for c in self.all_cursors() if c.hasSelection()] 218 if texts: 219 QApplication.clipboard().setText("\n".join(texts)) 220 221 def cut(self): 222 if not self.extra_cursors: 223 super().cut() 224 return 225 self.copy() 226 self._at_each_cursor(lambda c: c.removeSelectedText()) 227 228 def paste(self): 229 if not self.extra_cursors: 230 super().paste() 231 return 232 text = QApplication.clipboard().text().replace("\r\n", "\n") 233 lines = text.split("\n") 234 cursors = self.all_cursors() 235 # As many lines as cursors: one each (e.g. copied with these cursors). 236 pieces = dict(zip((c.position() for c in cursors), lines)) if len(lines) == len(cursors) else None 237 238 def insert(c): 239 c.insertText(pieces[c.position()] if pieces is not None and c.position() in pieces else text) 240 241 self._at_each_cursor(insert)
15class MultiCursorMixin: 16 """Several cursors at once (a part of `vibefpmgui.code_editor.CodeEditor`).""" 17 18 def all_cursors(self) -> list[QTextCursor]: 19 """Every cursor, the main one included, in the order they're in the text.""" 20 return sorted([self.textCursor(), *self.extra_cursors], key=lambda c: c.position()) 21 22 def toggle_cursor_at(self, position: int): 23 """⌥/Alt-click: add a cursor there, or remove the one that's there.""" 24 for c in self.extra_cursors: 25 if c.position() == position: 26 self.extra_cursors.remove(c) 27 self._cursors_changed() 28 return 29 if position == self.textCursor().position() and not self.textCursor().hasSelection(): 30 return # (the main cursor is already there) 31 cursor = QTextCursor(self.document()) 32 cursor.setPosition(position) 33 self.extra_cursors.append(cursor) 34 self._cursors_changed() 35 36 def add_column_cursors(self, to_line: int, to_column: int | None = None): 37 """⌘⇧/Ctrl+Shift-click: a cursor on each line from the main cursor's 38 line to ``to_line`` (0-based). Clicked in the main cursor's column: 39 cursors in that column; in another (``to_column``): a box selection, 40 each line selected from the one column to the other, its cursor at 41 the clicked end (both clipped to a shorter line's end).""" 42 main = self.textCursor() 43 first, column = main.blockNumber(), main.positionInBlock() 44 end_column = column if to_column is None else to_column 45 46 def on_line(block) -> QTextCursor: 47 last = block.length() - 1 # (the line's end, before the line break) 48 cursor = QTextCursor(block) 49 cursor.setPosition(block.position() + min(column, last)) 50 cursor.setPosition(block.position() + min(end_column, last), QTextCursor.KeepAnchor) 51 return cursor 52 53 self.setTextCursor(on_line(main.block())) 54 step = 1 if to_line >= first else -1 55 for number in range(first + step, to_line + step, step): 56 block = self.document().findBlockByNumber(number) 57 if not block.isValid(): 58 break 59 self.extra_cursors.append(on_line(block)) 60 self._cursors_changed() 61 62 def clear_extra_cursors(self): 63 if self.extra_cursors: 64 self.extra_cursors = [] 65 self._cursors_changed() 66 67 def _cursors_changed(self): 68 # Two cursors on the same spot are one (e.g. after deleting between them). 69 seen = {(self.textCursor().position(), self.textCursor().anchor())} 70 kept = [] 71 for c in self.extra_cursors: 72 key = (c.position(), c.anchor()) 73 if key not in seen: 74 seen.add(key) 75 kept.append(c) 76 self.extra_cursors = kept 77 if kept and self.cursorWidth(): # several cursors: draw them all ourselves 78 self._caret_width = self.cursorWidth() 79 self.setCursorWidth(0) 80 elif not kept and not self.cursorWidth(): # one again: Qt's own 81 self.setCursorWidth(self._caret_width) 82 self._restart_blink() 83 self.update_extra_selections(rebuild=False) # (the cursors' selections are always made anew) 84 self.viewport().update() 85 self.cursors_changed.emit() 86 87 def _restart_blink(self): 88 """Typing or moving shows the cursors and starts the blink again (as 89 Qt does with its own).""" 90 if not self.extra_cursors: 91 self._caret_timer.stop() 92 return 93 self._caret_on = True 94 flash = QApplication.cursorFlashTime() # (a whole on-off cycle; 0: no blinking) 95 if flash > 0: 96 self._caret_timer.start(flash // 2) 97 else: 98 self._caret_timer.stop() 99 self.viewport().update() 100 101 def _blink(self): 102 self._caret_on = not self._caret_on 103 self.viewport().update() 104 105 def _at_each_cursor(self, edit): 106 """``edit(cursor)`` at every cursor, as one Undo step.""" 107 main = self.textCursor() 108 cursors = [main, *self.extra_cursors] 109 main.beginEditBlock() # (the document's: every cursor's edits are in it) 110 for c in sorted(cursors, key=lambda c: c.position(), reverse=True): 111 edit(c) 112 main.endEditBlock() 113 self.setTextCursor(main) 114 self._cursors_changed() 115 116 def visual_column(self, cursor: QTextCursor) -> int: 117 """The column a position shows at (a tab: to the next tab stop).""" 118 tab = max(1, round(self.tabStopDistance() / max(1, self.fontMetrics().horizontalAdvance(" ")))) 119 column = 0 120 for ch in cursor.block().text()[:cursor.positionInBlock()]: 121 column = (column // tab + 1) * tab if ch == "\t" else column + 1 122 return column 123 124 def align_cursors(self) -> bool: 125 """Edit → Lines → Align Cursors: spaces before each cursor left of the 126 right-most one, so they're all in its column (one Undo step). Several 127 cursors on a line: the first on each line aligned, then the second, and 128 so on. A cursor with a selection: by its left edge (the selection moves 129 along). False: fewer than two cursors (nothing to align).""" 130 cursors = self.all_cursors() 131 if len(cursors) < 2: 132 return False 133 lines: dict[int, list[QTextCursor]] = {} 134 for c in cursors: 135 edge = QTextCursor(c) # (where the spaces go: a selection's left edge) 136 edge.setPosition(c.selectionStart()) 137 lines.setdefault(edge.blockNumber(), []).append(edge) 138 for row in lines.values(): 139 row.sort(key=lambda c: c.position()) 140 main = self.textCursor() 141 main.beginEditBlock() 142 for k in range(max(len(row) for row in lines.values())): 143 column_k = [row[k] for row in lines.values() if len(row) > k] 144 target = max(self.visual_column(c) for c in column_k) 145 for c in column_k: 146 gap = target - self.visual_column(c) 147 if gap > 0: 148 c.insertText(" " * gap) # (the cursors after it on the line move along) 149 main.endEditBlock() 150 self.setTextCursor(main) 151 self._cursors_changed() 152 return True 153 154 def _move_each_cursor(self, operation, select: bool): 155 mode = QTextCursor.KeepAnchor if select else QTextCursor.MoveAnchor 156 main = self.textCursor() 157 for c in (main, *self.extra_cursors): 158 c.movePosition(operation, mode) 159 self.setTextCursor(main) 160 self._cursors_changed() 161 162 def _multi_cursor_key(self, event) -> bool: 163 """A key with several cursors: True if it was handled here.""" 164 self.completion_popup.hide() 165 self.signature_popup.hide() 166 key, mods = event.key(), event.modifiers() 167 shift = bool(mods & Qt.ShiftModifier) 168 word = bool(mods & (Qt.AltModifier if sys.platform == "darwin" else Qt.ControlModifier)) 169 if key == Qt.Key_Escape: 170 self.clear_extra_cursors() 171 return True 172 if event.matches(QKeySequence.Copy): 173 self.copy() 174 return True 175 if event.matches(QKeySequence.Cut): 176 self.cut() 177 return True 178 if event.matches(QKeySequence.Paste): 179 self.paste() 180 return True 181 moves = {Qt.Key_Left: QTextCursor.WordLeft if word else QTextCursor.Left, 182 Qt.Key_Right: QTextCursor.WordRight if word else QTextCursor.Right, 183 Qt.Key_Up: QTextCursor.Up, Qt.Key_Down: QTextCursor.Down, 184 Qt.Key_Home: QTextCursor.StartOfBlock, Qt.Key_End: QTextCursor.EndOfBlock} 185 command = sys.platform == "darwin" and mods & Qt.ControlModifier 186 if command and key in (Qt.Key_Left, Qt.Key_Right): # ⌘← / ⌘→: the start / end of the line 187 self._move_each_cursor(QTextCursor.StartOfBlock if key == Qt.Key_Left else QTextCursor.EndOfBlock, 188 shift) 189 return True 190 if key in moves and not command: 191 self._move_each_cursor(moves[key], shift) 192 return True 193 if key == Qt.Key_Backspace: 194 self._at_each_cursor(lambda c: c.removeSelectedText() if c.hasSelection() else c.deletePreviousChar()) 195 return True 196 if key == Qt.Key_Delete: 197 self._at_each_cursor(lambda c: c.removeSelectedText() if c.hasSelection() else c.deleteChar()) 198 return True 199 if key in (Qt.Key_Return, Qt.Key_Enter): 200 def newline(c): 201 line = c.block().text() 202 c.insertText("\n" + line[:len(line) - len(line.lstrip())]) # (keeping the indentation) 203 self._at_each_cursor(newline) 204 return True 205 if key == Qt.Key_Tab: 206 self._at_each_cursor(lambda c: c.insertText(self.tab_text(c))) 207 return True 208 text = event.text() 209 if text and text.isprintable() and not mods & (Qt.ControlModifier | Qt.MetaModifier): 210 self._at_each_cursor(lambda c: c.insertText(text)) 211 return True 212 return False # (other keys: shortcuts and the like, for the main cursor) 213 214 def copy(self): 215 if not self.extra_cursors: 216 super().copy() 217 return 218 texts = [c.selectedText().replace("\u2029", "\n") for c in self.all_cursors() if c.hasSelection()] 219 if texts: 220 QApplication.clipboard().setText("\n".join(texts)) 221 222 def cut(self): 223 if not self.extra_cursors: 224 super().cut() 225 return 226 self.copy() 227 self._at_each_cursor(lambda c: c.removeSelectedText()) 228 229 def paste(self): 230 if not self.extra_cursors: 231 super().paste() 232 return 233 text = QApplication.clipboard().text().replace("\r\n", "\n") 234 lines = text.split("\n") 235 cursors = self.all_cursors() 236 # As many lines as cursors: one each (e.g. copied with these cursors). 237 pieces = dict(zip((c.position() for c in cursors), lines)) if len(lines) == len(cursors) else None 238 239 def insert(c): 240 c.insertText(pieces[c.position()] if pieces is not None and c.position() in pieces else text) 241 242 self._at_each_cursor(insert)
Several cursors at once (a part of vibefpmgui.code_editor.CodeEditor).
18 def all_cursors(self) -> list[QTextCursor]: 19 """Every cursor, the main one included, in the order they're in the text.""" 20 return sorted([self.textCursor(), *self.extra_cursors], key=lambda c: c.position())
Every cursor, the main one included, in the order they're in the text.
22 def toggle_cursor_at(self, position: int): 23 """⌥/Alt-click: add a cursor there, or remove the one that's there.""" 24 for c in self.extra_cursors: 25 if c.position() == position: 26 self.extra_cursors.remove(c) 27 self._cursors_changed() 28 return 29 if position == self.textCursor().position() and not self.textCursor().hasSelection(): 30 return # (the main cursor is already there) 31 cursor = QTextCursor(self.document()) 32 cursor.setPosition(position) 33 self.extra_cursors.append(cursor) 34 self._cursors_changed()
⌥/Alt-click: add a cursor there, or remove the one that's there.
36 def add_column_cursors(self, to_line: int, to_column: int | None = None): 37 """⌘⇧/Ctrl+Shift-click: a cursor on each line from the main cursor's 38 line to ``to_line`` (0-based). Clicked in the main cursor's column: 39 cursors in that column; in another (``to_column``): a box selection, 40 each line selected from the one column to the other, its cursor at 41 the clicked end (both clipped to a shorter line's end).""" 42 main = self.textCursor() 43 first, column = main.blockNumber(), main.positionInBlock() 44 end_column = column if to_column is None else to_column 45 46 def on_line(block) -> QTextCursor: 47 last = block.length() - 1 # (the line's end, before the line break) 48 cursor = QTextCursor(block) 49 cursor.setPosition(block.position() + min(column, last)) 50 cursor.setPosition(block.position() + min(end_column, last), QTextCursor.KeepAnchor) 51 return cursor 52 53 self.setTextCursor(on_line(main.block())) 54 step = 1 if to_line >= first else -1 55 for number in range(first + step, to_line + step, step): 56 block = self.document().findBlockByNumber(number) 57 if not block.isValid(): 58 break 59 self.extra_cursors.append(on_line(block)) 60 self._cursors_changed()
⌘⇧/Ctrl+Shift-click: a cursor on each line from the main cursor's
line to to_line (0-based). Clicked in the main cursor's column:
cursors in that column; in another (to_column): a box selection,
each line selected from the one column to the other, its cursor at
the clicked end (both clipped to a shorter line's end).
116 def visual_column(self, cursor: QTextCursor) -> int: 117 """The column a position shows at (a tab: to the next tab stop).""" 118 tab = max(1, round(self.tabStopDistance() / max(1, self.fontMetrics().horizontalAdvance(" ")))) 119 column = 0 120 for ch in cursor.block().text()[:cursor.positionInBlock()]: 121 column = (column // tab + 1) * tab if ch == "\t" else column + 1 122 return column
The column a position shows at (a tab: to the next tab stop).
124 def align_cursors(self) -> bool: 125 """Edit → Lines → Align Cursors: spaces before each cursor left of the 126 right-most one, so they're all in its column (one Undo step). Several 127 cursors on a line: the first on each line aligned, then the second, and 128 so on. A cursor with a selection: by its left edge (the selection moves 129 along). False: fewer than two cursors (nothing to align).""" 130 cursors = self.all_cursors() 131 if len(cursors) < 2: 132 return False 133 lines: dict[int, list[QTextCursor]] = {} 134 for c in cursors: 135 edge = QTextCursor(c) # (where the spaces go: a selection's left edge) 136 edge.setPosition(c.selectionStart()) 137 lines.setdefault(edge.blockNumber(), []).append(edge) 138 for row in lines.values(): 139 row.sort(key=lambda c: c.position()) 140 main = self.textCursor() 141 main.beginEditBlock() 142 for k in range(max(len(row) for row in lines.values())): 143 column_k = [row[k] for row in lines.values() if len(row) > k] 144 target = max(self.visual_column(c) for c in column_k) 145 for c in column_k: 146 gap = target - self.visual_column(c) 147 if gap > 0: 148 c.insertText(" " * gap) # (the cursors after it on the line move along) 149 main.endEditBlock() 150 self.setTextCursor(main) 151 self._cursors_changed() 152 return True
Edit → Lines → Align Cursors: spaces before each cursor left of the right-most one, so they're all in its column (one Undo step). Several cursors on a line: the first on each line aligned, then the second, and so on. A cursor with a selection: by its left edge (the selection moves along). False: fewer than two cursors (nothing to align).
229 def paste(self): 230 if not self.extra_cursors: 231 super().paste() 232 return 233 text = QApplication.clipboard().text().replace("\r\n", "\n") 234 lines = text.split("\n") 235 cursors = self.all_cursors() 236 # As many lines as cursors: one each (e.g. copied with these cursors). 237 pieces = dict(zip((c.position() for c in cursors), lines)) if len(lines) == len(cursors) else None 238 239 def insert(c): 240 c.insertText(pieces[c.position()] if pieces is not None and c.position() in pieces else text) 241 242 self._at_each_cursor(insert)