vibefpmgui.rename_dialog
Preview of a Rename Symbol: which files and lines will change.
Each file has a checkbox. Files outside the project (dependencies) start unticked: fpm re-downloads git dependencies, so edits there would be lost, and a dependency is usually not yours to change. Each changed line is shown as it will read, in the editor's syntax colors (as in the Search panel), the new name highlighted.
1"""Preview of a Rename Symbol: which files and lines will change. 2 3Each file has a checkbox. Files outside the project (dependencies) start 4unticked: fpm re-downloads git dependencies, so edits there would be lost, 5and a dependency is usually not yours to change. Each changed line is shown 6as it will read, in the editor's syntax colors (as in the Search panel), the 7new name highlighted. 8""" 9 10from __future__ import annotations 11 12from collections.abc import Callable 13from pathlib import Path 14 15from pygments.styles import get_style_by_name 16from qtpy.QtCore import QRect, Qt 17from qtpy.QtGui import QColor, QFont, QFontMetrics, QPalette 18from qtpy.QtWidgets import ( 19 QApplication, 20 QDialog, 21 QDialogButtonBox, 22 QLabel, 23 QStyle, 24 QStyledItemDelegate, 25 QStyleOptionViewItem, 26 QTreeWidget, 27 QTreeWidgetItem, 28 QVBoxLayout, 29) 30 31from . import highlighter as hl 32from . import lsp as lsp_mod 33from .references_panel import short_path 34from .result_tree import MATCH_COLOR, paint_code, syntax_formats 35 36PATH_ROLE = Qt.UserRole + 1 37LINE_ROLE = Qt.UserRole + 2 # a changed line's row: (file, line (0-based), the new name's (start, end) in it) 38 39 40def renamed_spans(edits: list[lsp_mod.TextEdit], line: int) -> list[tuple[int, int]]: 41 """Where a line's edits put their new text, in the line as it will read 42 (each edit shifts those after it on the line).""" 43 spans, shift = [], 0 44 for edit in sorted((e for e in edits if e.line == line == e.end_line), key=lambda e: e.column): 45 start = edit.column + shift 46 spans.append((start, start + len(edit.new_text))) 47 shift += len(edit.new_text) - (edit.end_column - edit.column) 48 return spans 49 50 51class _LineDelegate(QStyledItemDelegate): 52 """A changed line: its number, then the line in the editor's font and 53 syntax colors, the new name highlighted (plain on the selected row).""" 54 55 def __init__(self, dialog: RenamePreviewDialog): 56 super().__init__(dialog.tree) 57 self.dialog = dialog 58 59 def paint(self, painter, option, index): 60 where = index.data(LINE_ROLE) 61 if where is None: 62 super().paint(painter, option, index) 63 return 64 path, line, spans = where 65 opt = QStyleOptionViewItem(option) 66 self.initStyleOption(opt, index) 67 opt.text = "" 68 style = opt.widget.style() if opt.widget else QApplication.style() 69 style.drawControl(QStyle.CE_ItemViewItem, opt, painter, opt.widget) 70 selected = bool(opt.state & QStyle.State_Selected) 71 group = QPalette.Active if opt.state & QStyle.State_Active else QPalette.Inactive 72 plain = opt.palette.color(group, QPalette.HighlightedText if selected else QPalette.Text) 73 dim = QColor(plain) 74 dim.setAlpha(140) 75 mono = self.dialog.mono 76 full = self.dialog.lines(path)[line] if line < len(self.dialog.lines(path)) else "" 77 code = full.lstrip() 78 column = len(full) - len(code) 79 painter.save() 80 painter.setClipRect(opt.rect) 81 number = f"{line + 1:>5} " 82 painter.setFont(mono) 83 painter.setPen(dim) 84 width = QFontMetrics(mono).horizontalAdvance(number) 85 painter.drawText(QRect(opt.rect.left() + 4, opt.rect.top(), width, opt.rect.height()), 86 Qt.AlignVCenter | Qt.AlignLeft, number) 87 left = opt.rect.left() + 4 + width 88 metrics = QFontMetrics(mono) 89 for start, end in spans: # (the new name: highlighted, as the Search panel's matches) 90 a, b = max(start - column, 0), max(end - column, 0) 91 x = left + metrics.horizontalAdvance(code[:a]) 92 painter.fillRect(QRect(x, opt.rect.top() + 2, metrics.horizontalAdvance(code[a:b]), 93 opt.rect.height() - 4), MATCH_COLOR) 94 formats = None if selected else self.dialog.line_formats(path, line) 95 paint_code(painter, QRect(left, opt.rect.top(), opt.rect.right() - left, opt.rect.height()), code, 96 formats, column, mono, plain) 97 painter.restore() 98 99 100def is_dependency(path: Path, project_dir: Path | None) -> bool: 101 if project_dir is None: 102 return False 103 root = project_dir.resolve() 104 if root not in path.parents: 105 return True 106 return "dependencies" in path.relative_to(root).parts[:2] # build/dependencies/... 107 108 109class RenamePreviewDialog(QDialog): 110 def __init__(self, old: str, new: str, edits: dict[Path, list[lsp_mod.TextEdit]], 111 file_text: Callable[[Path], str], open_files: set[Path], project_dir: Path | None, 112 mono: QFont | None = None, parent=None): 113 super().__init__(parent) 114 self.setWindowTitle("Rename Symbol") 115 self.resize(760, 480) 116 count = sum(len(e) for e in edits.values()) 117 intro = QLabel(f"Rename <b>{old}</b> to <b>{new}</b>: {count} change{'s' * (count != 1)} in " 118 f"{len(edits)} file{'s' * (len(edits) != 1)}. Files open in the editor are changed " 119 "there (unsaved, Undo reverts); other files are saved straight away.") 120 intro.setWordWrap(True) 121 self.mono = mono or QFont() 122 self._lines: dict[str, list[str]] = {} # file -> its lines as they will read 123 self._style = get_style_by_name(hl.style_name(QApplication.palette())) 124 self._formats: dict = {} # token type -> format 125 self._line_formats: dict[tuple[str, int], list | None] = {} 126 self.tree = QTreeWidget() 127 self.tree.setHeaderHidden(True) 128 self.tree.setUniformRowHeights(True) 129 self.tree.setItemDelegate(_LineDelegate(self)) 130 deps = False 131 for path in sorted(edits, key=lambda p: (is_dependency(p, project_dir), short_path(p, project_dir))): 132 file_edits = edits[path] 133 dependency = is_dependency(path, project_dir) 134 deps |= dependency 135 notes = [] 136 if path in open_files: 137 notes.append("open in the editor") 138 if dependency: 139 notes.append("dependency — not changed unless ticked") 140 note = f" — {', '.join(notes)}" if notes else "" 141 node = QTreeWidgetItem([f"{short_path(path, project_dir)} ({len(file_edits)}){note}"]) 142 node.setFlags(node.flags() | Qt.ItemIsUserCheckable) 143 node.setCheckState(0, Qt.Unchecked if dependency else Qt.Checked) 144 node.setData(0, PATH_ROLE, str(path)) 145 node.setToolTip(0, str(path)) 146 self.tree.addTopLevelItem(node) 147 # The changed lines, as they will read afterwards. 148 try: 149 old_lines = file_text(path).split("\n") 150 except OSError: 151 old_lines = [] 152 new_lines = lsp_mod.apply_edits("\n".join(old_lines), file_edits).split("\n") 153 self._lines[str(path)] = new_lines 154 for line in sorted({e.line for e in file_edits}): 155 after = new_lines[line].strip() if line < len(new_lines) else "" 156 before = old_lines[line].strip() if line < len(old_lines) else "" 157 child = QTreeWidgetItem([f"{line + 1}: {after}"]) 158 child.setToolTip(0, f"Before: {before}\nAfter: {after}") 159 child.setData(0, LINE_ROLE, (str(path), line, renamed_spans(file_edits, line))) 160 if mono is not None: 161 child.setFont(0, mono) 162 child.setFlags(child.flags() & ~Qt.ItemIsUserCheckable) 163 node.addChild(child) 164 node.setExpanded(len(edits) <= 6) 165 buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 166 buttons.button(QDialogButtonBox.Ok).setText("Rename") 167 buttons.accepted.connect(self.accept) 168 buttons.rejected.connect(self.reject) 169 layout = QVBoxLayout(self) 170 layout.addWidget(intro) 171 layout.addWidget(self.tree, 1) 172 if deps: 173 warn = QLabel("Files in dependencies are left unticked: fpm may download them again " 174 "(losing the change), and the rest of that package wouldn't be renamed.") 175 warn.setWordWrap(True) 176 warn.setStyleSheet("color: gray;") 177 layout.addWidget(warn) 178 layout.addWidget(buttons) 179 180 def lines(self, path: str) -> list[str]: 181 return self._lines.get(path, []) 182 183 def line_formats(self, path: str, line: int) -> list | None: 184 """The syntax format of each column of a changed line, as it will read 185 (lexed with the lines before it, as the Search panel's).""" 186 key = (path, line) 187 if key not in self._line_formats: 188 self._line_formats[key] = syntax_formats(self.lines(path), line + 1, Path(path), self._style, 189 self._formats) 190 return self._line_formats[key] 191 192 def selected(self) -> list[Path]: 193 out = [] 194 for i in range(self.tree.topLevelItemCount()): 195 node = self.tree.topLevelItem(i) 196 if node.checkState(0) == Qt.Checked: 197 out.append(Path(node.data(0, PATH_ROLE))) 198 return out
41def renamed_spans(edits: list[lsp_mod.TextEdit], line: int) -> list[tuple[int, int]]: 42 """Where a line's edits put their new text, in the line as it will read 43 (each edit shifts those after it on the line).""" 44 spans, shift = [], 0 45 for edit in sorted((e for e in edits if e.line == line == e.end_line), key=lambda e: e.column): 46 start = edit.column + shift 47 spans.append((start, start + len(edit.new_text))) 48 shift += len(edit.new_text) - (edit.end_column - edit.column) 49 return spans
Where a line's edits put their new text, in the line as it will read (each edit shifts those after it on the line).
110class RenamePreviewDialog(QDialog): 111 def __init__(self, old: str, new: str, edits: dict[Path, list[lsp_mod.TextEdit]], 112 file_text: Callable[[Path], str], open_files: set[Path], project_dir: Path | None, 113 mono: QFont | None = None, parent=None): 114 super().__init__(parent) 115 self.setWindowTitle("Rename Symbol") 116 self.resize(760, 480) 117 count = sum(len(e) for e in edits.values()) 118 intro = QLabel(f"Rename <b>{old}</b> to <b>{new}</b>: {count} change{'s' * (count != 1)} in " 119 f"{len(edits)} file{'s' * (len(edits) != 1)}. Files open in the editor are changed " 120 "there (unsaved, Undo reverts); other files are saved straight away.") 121 intro.setWordWrap(True) 122 self.mono = mono or QFont() 123 self._lines: dict[str, list[str]] = {} # file -> its lines as they will read 124 self._style = get_style_by_name(hl.style_name(QApplication.palette())) 125 self._formats: dict = {} # token type -> format 126 self._line_formats: dict[tuple[str, int], list | None] = {} 127 self.tree = QTreeWidget() 128 self.tree.setHeaderHidden(True) 129 self.tree.setUniformRowHeights(True) 130 self.tree.setItemDelegate(_LineDelegate(self)) 131 deps = False 132 for path in sorted(edits, key=lambda p: (is_dependency(p, project_dir), short_path(p, project_dir))): 133 file_edits = edits[path] 134 dependency = is_dependency(path, project_dir) 135 deps |= dependency 136 notes = [] 137 if path in open_files: 138 notes.append("open in the editor") 139 if dependency: 140 notes.append("dependency — not changed unless ticked") 141 note = f" — {', '.join(notes)}" if notes else "" 142 node = QTreeWidgetItem([f"{short_path(path, project_dir)} ({len(file_edits)}){note}"]) 143 node.setFlags(node.flags() | Qt.ItemIsUserCheckable) 144 node.setCheckState(0, Qt.Unchecked if dependency else Qt.Checked) 145 node.setData(0, PATH_ROLE, str(path)) 146 node.setToolTip(0, str(path)) 147 self.tree.addTopLevelItem(node) 148 # The changed lines, as they will read afterwards. 149 try: 150 old_lines = file_text(path).split("\n") 151 except OSError: 152 old_lines = [] 153 new_lines = lsp_mod.apply_edits("\n".join(old_lines), file_edits).split("\n") 154 self._lines[str(path)] = new_lines 155 for line in sorted({e.line for e in file_edits}): 156 after = new_lines[line].strip() if line < len(new_lines) else "" 157 before = old_lines[line].strip() if line < len(old_lines) else "" 158 child = QTreeWidgetItem([f"{line + 1}: {after}"]) 159 child.setToolTip(0, f"Before: {before}\nAfter: {after}") 160 child.setData(0, LINE_ROLE, (str(path), line, renamed_spans(file_edits, line))) 161 if mono is not None: 162 child.setFont(0, mono) 163 child.setFlags(child.flags() & ~Qt.ItemIsUserCheckable) 164 node.addChild(child) 165 node.setExpanded(len(edits) <= 6) 166 buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 167 buttons.button(QDialogButtonBox.Ok).setText("Rename") 168 buttons.accepted.connect(self.accept) 169 buttons.rejected.connect(self.reject) 170 layout = QVBoxLayout(self) 171 layout.addWidget(intro) 172 layout.addWidget(self.tree, 1) 173 if deps: 174 warn = QLabel("Files in dependencies are left unticked: fpm may download them again " 175 "(losing the change), and the rest of that package wouldn't be renamed.") 176 warn.setWordWrap(True) 177 warn.setStyleSheet("color: gray;") 178 layout.addWidget(warn) 179 layout.addWidget(buttons) 180 181 def lines(self, path: str) -> list[str]: 182 return self._lines.get(path, []) 183 184 def line_formats(self, path: str, line: int) -> list | None: 185 """The syntax format of each column of a changed line, as it will read 186 (lexed with the lines before it, as the Search panel's).""" 187 key = (path, line) 188 if key not in self._line_formats: 189 self._line_formats[key] = syntax_formats(self.lines(path), line + 1, Path(path), self._style, 190 self._formats) 191 return self._line_formats[key] 192 193 def selected(self) -> list[Path]: 194 out = [] 195 for i in range(self.tree.topLevelItemCount()): 196 node = self.tree.topLevelItem(i) 197 if node.checkState(0) == Qt.Checked: 198 out.append(Path(node.data(0, PATH_ROLE))) 199 return out
QDialog(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None
111 def __init__(self, old: str, new: str, edits: dict[Path, list[lsp_mod.TextEdit]], 112 file_text: Callable[[Path], str], open_files: set[Path], project_dir: Path | None, 113 mono: QFont | None = None, parent=None): 114 super().__init__(parent) 115 self.setWindowTitle("Rename Symbol") 116 self.resize(760, 480) 117 count = sum(len(e) for e in edits.values()) 118 intro = QLabel(f"Rename <b>{old}</b> to <b>{new}</b>: {count} change{'s' * (count != 1)} in " 119 f"{len(edits)} file{'s' * (len(edits) != 1)}. Files open in the editor are changed " 120 "there (unsaved, Undo reverts); other files are saved straight away.") 121 intro.setWordWrap(True) 122 self.mono = mono or QFont() 123 self._lines: dict[str, list[str]] = {} # file -> its lines as they will read 124 self._style = get_style_by_name(hl.style_name(QApplication.palette())) 125 self._formats: dict = {} # token type -> format 126 self._line_formats: dict[tuple[str, int], list | None] = {} 127 self.tree = QTreeWidget() 128 self.tree.setHeaderHidden(True) 129 self.tree.setUniformRowHeights(True) 130 self.tree.setItemDelegate(_LineDelegate(self)) 131 deps = False 132 for path in sorted(edits, key=lambda p: (is_dependency(p, project_dir), short_path(p, project_dir))): 133 file_edits = edits[path] 134 dependency = is_dependency(path, project_dir) 135 deps |= dependency 136 notes = [] 137 if path in open_files: 138 notes.append("open in the editor") 139 if dependency: 140 notes.append("dependency — not changed unless ticked") 141 note = f" — {', '.join(notes)}" if notes else "" 142 node = QTreeWidgetItem([f"{short_path(path, project_dir)} ({len(file_edits)}){note}"]) 143 node.setFlags(node.flags() | Qt.ItemIsUserCheckable) 144 node.setCheckState(0, Qt.Unchecked if dependency else Qt.Checked) 145 node.setData(0, PATH_ROLE, str(path)) 146 node.setToolTip(0, str(path)) 147 self.tree.addTopLevelItem(node) 148 # The changed lines, as they will read afterwards. 149 try: 150 old_lines = file_text(path).split("\n") 151 except OSError: 152 old_lines = [] 153 new_lines = lsp_mod.apply_edits("\n".join(old_lines), file_edits).split("\n") 154 self._lines[str(path)] = new_lines 155 for line in sorted({e.line for e in file_edits}): 156 after = new_lines[line].strip() if line < len(new_lines) else "" 157 before = old_lines[line].strip() if line < len(old_lines) else "" 158 child = QTreeWidgetItem([f"{line + 1}: {after}"]) 159 child.setToolTip(0, f"Before: {before}\nAfter: {after}") 160 child.setData(0, LINE_ROLE, (str(path), line, renamed_spans(file_edits, line))) 161 if mono is not None: 162 child.setFont(0, mono) 163 child.setFlags(child.flags() & ~Qt.ItemIsUserCheckable) 164 node.addChild(child) 165 node.setExpanded(len(edits) <= 6) 166 buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 167 buttons.button(QDialogButtonBox.Ok).setText("Rename") 168 buttons.accepted.connect(self.accept) 169 buttons.rejected.connect(self.reject) 170 layout = QVBoxLayout(self) 171 layout.addWidget(intro) 172 layout.addWidget(self.tree, 1) 173 if deps: 174 warn = QLabel("Files in dependencies are left unticked: fpm may download them again " 175 "(losing the change), and the rest of that package wouldn't be renamed.") 176 warn.setWordWrap(True) 177 warn.setStyleSheet("color: gray;") 178 layout.addWidget(warn) 179 layout.addWidget(buttons)
__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None
Initialize self. See help(type(self)) for accurate signature.
184 def line_formats(self, path: str, line: int) -> list | None: 185 """The syntax format of each column of a changed line, as it will read 186 (lexed with the lines before it, as the Search panel's).""" 187 key = (path, line) 188 if key not in self._line_formats: 189 self._line_formats[key] = syntax_formats(self.lines(path), line + 1, Path(path), self._style, 190 self._formats) 191 return self._line_formats[key]
The syntax format of each column of a changed line, as it will read (lexed with the lines before it, as the Search panel's).