Coverage for vibefpmgui/window/editor_tabs.py: 90%

414 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

1"""The editor's tabs: opening, previewing, closing, renaming and deleting files. 

2 

3Also moving around them: Go to a location, Go Back and Go Forward. 

4""" 

5 

6from __future__ import annotations 

7 

8import os 

9from pathlib import Path 

10 

11from qtpy.QtCore import QFile, QPoint, QRect, QSize, Qt, QUrl, Signal 

12from qtpy.QtGui import ( 

13 QAction, 

14 QActionGroup, 

15 QColor, 

16 QDesktopServices, 

17 QFont, 

18 QFontMetrics, 

19 QKeySequence, 

20) 

21from qtpy.QtWidgets import ( 

22 QFileDialog, 

23 QHBoxLayout, 

24 QInputDialog, 

25 QLabel, 

26 QMenu, 

27 QMessageBox, 

28 QStyle, 

29 QStyleOptionTab, 

30 QStylePainter, 

31 QTabBar, 

32 QTabWidget, 

33 QToolButton, 

34 QVBoxLayout, 

35 QWidget, 

36) 

37 

38from ..editor import EditorTab 

39from ..icons import make_icon 

40from ..wrapping_tabs import WrappingTabBar 

41 

42 

43class ScrollingTabBar(QTabBar): 

44 """A tab bar where the mouse wheel or a trackpad scrolls the tabs 

45 sideways when they don't all fit, rather than switching tabs; a preview 

46 tab's name is in italics (see set_italic).""" 

47 

48 changed = Signal() # a tab added, removed or renamed (for the tabs on several rows: WrappingTabBar) 

49 empty_menu_requested = Signal(QPoint) # a right-click beside the tabs (on none): where, on the screen 

50 

51 def set_tab_name(self, index: int, name: str, italic: bool = False): 

52 """A tab's name, in italics or not. Qt has no font per tab: an italic 

53 one's own text is left empty, and its name drawn here (paintEvent).""" 

54 self.setTabData(index, name if italic else None) 

55 self.setTabText(index, "" if italic else name) 

56 self.updateGeometry() 

57 self.update() 

58 self.changed.emit() 

59 

60 def tabInserted(self, index): 

61 super().tabInserted(index) 

62 self.changed.emit() 

63 

64 def tabRemoved(self, index): 

65 super().tabRemoved(index) 

66 self.changed.emit() 

67 

68 def tab_name(self, index: int) -> str: 

69 return self.italic_name(index) or self.tabText(index) 

70 

71 def italic_name(self, index: int) -> str | None: 

72 name = self.tabData(index) 

73 return name if isinstance(name, str) else None 

74 

75 def _italic_font(self) -> QFont: 

76 font = QFont(self.font()) 

77 font.setItalic(True) 

78 return font 

79 

80 NAME_GAP = 6 # between the name and the icon or close button beside it 

81 

82 def tabSizeHint(self, index): 

83 size = super().tabSizeHint(index) 

84 name = self.italic_name(index) 

85 if name is not None: # (room for the name it doesn't have as text) 

86 size.setWidth(size.width() + QFontMetrics(self._italic_font()).horizontalAdvance(name) + 4) 

87 # No more room around the name than NAME_GAP on each side: some styles 

88 # (macOS's) leave its buttons at the tab's edges and center the name in 

89 # a much wider space. As wide as the style leaves for the name, less 

90 # what's spare (a style that leaves none, as Fusion, is unchanged). 

91 option = QStyleOptionTab() 

92 self.initStyleOption(option, index) 

93 option.rect = QRect(QPoint(0, 0), size) 

94 room = self.style().subElementRect(QStyle.SE_TabBarTabText, option, self).width() 

95 font = self._italic_font() if name is not None else self.font() 

96 spare = room - QFontMetrics(font).horizontalAdvance(self.tab_name(index)) - 2 * self.NAME_GAP 

97 if spare > 0: 

98 size.setWidth(size.width() - spare) 

99 return size 

100 

101 def paintEvent(self, event): 

102 super().paintEvent(event) 

103 names = [(i, self.italic_name(i)) for i in range(self.count())] 

104 if not any(name for _i, name in names): 

105 return 

106 painter = QStylePainter(self) 

107 painter.setFont(self._italic_font()) 

108 for index, name in names: 

109 if name is not None: 

110 option = QStyleOptionTab() 

111 self.initStyleOption(option, index) 

112 option.text = name 

113 painter.drawControl(QStyle.CE_TabBarTabLabel, option) 

114 

115 def contextMenuEvent(self, event): 

116 if self.tabAt(event.pos()) < 0: 

117 self.empty_menu_requested.emit(event.globalPos()) 

118 event.accept() 

119 else: 

120 super().contextMenuEvent(event) 

121 

122 def wheelEvent(self, event): 

123 arrows = [b for b in self.findChildren(QToolButton) if b.isVisible() and b.arrowType() in 

124 (Qt.LeftArrow, Qt.RightArrow)] 

125 if not arrows: 

126 super().wheelEvent(event) 

127 return 

128 delta = event.angleDelta() 

129 step = delta.x() if abs(delta.x()) > abs(delta.y()) else delta.y() 

130 self._wheel_rest = getattr(self, "_wheel_rest", 0) + step 

131 while abs(self._wheel_rest) >= 120: # one notch (or that much trackpad travel) per tab 

132 back = self._wheel_rest > 0 

133 self._wheel_rest -= 120 if back else -120 

134 for button in arrows: 

135 if button.arrowType() == (Qt.LeftArrow if back else Qt.RightArrow) and button.isEnabled(): 

136 button.click() 

137 event.accept() 

138 

139 

140class EditorTabsMixin: 

141 """The editor's tabs (a part of `vibefpmgui.main_window.MainWindow`).""" 

142 

143 def _build_editor_tabs(self): 

144 """The editor's tabs (the middle of the window).""" 

145 self.tabs = QTabWidget() 

146 self.tabs.setTabBar(ScrollingTabBar()) # (before any tabs are added) 

147 self.tabs.setDocumentMode(True) 

148 # With many files open, scroll the tab bar (arrows, wheel or trackpad) 

149 # rather than squeezing every tab; macOS's style turns the arrows off 

150 # and shortens the names by default. 

151 self.tabs.setUsesScrollButtons(True) 

152 self.tabs.setElideMode(Qt.ElideNone) 

153 self.tabs.tabBar().setExpanding(False) 

154 # A list of every open tab, at the right end of the tab bar. 

155 tab_list = self.tab_list = QMenu(self.tabs) 

156 tab_list.aboutToShow.connect(lambda: self._fill_tab_list(tab_list)) 

157 self.tab_list_button = self._tab_list_button() 

158 self.tabs.setCornerWidget(self.tab_list_button, Qt.TopRightCorner) 

159 # The middle of the window: files open in the editor, each in a closable tab. 

160 self.tabs.setTabsClosable(True) 

161 self.tabs.setMovable(True) 

162 self.tabs.tabCloseRequested.connect(self.close_editor) 

163 self.tabs.tabBarDoubleClicked.connect(lambda i: self._make_permanent(self.tabs.widget(i))) 

164 self.tabs.currentChanged.connect(self._update_editor_actions) 

165 self.tabs.currentChanged.connect(self._reveal_current_file) 

166 self.tabs.currentChanged.connect(self._update_central) 

167 self.tabs.currentChanged.connect(self._update_problems) 

168 # The tabs on several rows instead of one that scrolls (View → Editor Tabs): 

169 # above the tabs, which then hide their own bar. 

170 self.tab_rows = WrappingTabBar(self.tabs) 

171 self.tab_rows.close_requested.connect(self.close_editor) 

172 self.tabs.tabBar().changed.connect(self.tab_rows.refresh) 

173 # Right-click beside the tabs: the Editor Tabs choices (as in the View menu). 

174 self.tabs.tabBar().empty_menu_requested.connect(self._tabs_menu) 

175 self.tab_rows.empty_menu_requested.connect(self._tabs_menu) 

176 # (The tab list, with Close All Editor Tabs, at the rows' end: as at the one row's.) 

177 self.tab_rows_list_button = self._tab_list_button() 

178 rows = QHBoxLayout() 

179 rows.setContentsMargins(0, 0, 0, 0) 

180 rows.setSpacing(0) 

181 rows.addWidget(self.tab_rows, 1) 

182 rows.addWidget(self.tab_rows_list_button, 0, Qt.AlignTop) 

183 self.editor_area = QWidget() 

184 area = QVBoxLayout(self.editor_area) 

185 area.setContentsMargins(0, 0, 0, 0) 

186 area.setSpacing(0) 

187 area.addLayout(rows) 

188 area.addWidget(self.tabs, 1) 

189 menu = self.tab_rows_menu = self.view_menu.addMenu("Editor Tabs") # (kept: PySide would let it go) 

190 group = QActionGroup(menu) 

191 self.tab_rows_actions: dict[bool, QAction] = {} 

192 for wrap, text, tip in ((False, "One Row (Scroll)", ("The editor's tabs side by side, scrolling when they " 

193 "don't all fit (the wheel or a trackpad scrolls them)")), 

194 (True, "Several Rows (Wrap)", ("The editor's tabs on as many rows as they need, " 

195 "all in view"))): 

196 action = menu.addAction(text) 

197 action.setCheckable(True) 

198 action.setStatusTip(tip) 

199 action.triggered.connect(lambda _=False, w=wrap: self.set_tab_rows(w)) 

200 group.addAction(action) 

201 self.tab_rows_actions[wrap] = action 

202 self.set_tab_rows(self.settings.value("tab_rows_wrap", False, type=bool), save=False) 

203 

204 def _tab_list_button(self) -> QToolButton: 

205 """A button for the list of every open tab (the tab bar's corner, or the rows' end).""" 

206 button = QToolButton() 

207 button.setArrowType(Qt.DownArrow) 

208 button.setAutoRaise(True) 

209 button.setToolTip("All open tabs") 

210 button.setPopupMode(QToolButton.InstantPopup) 

211 # Just the one arrow (a button with a menu also gets a menu indicator). 

212 button.setStyleSheet("QToolButton::menu-indicator { image: none; width: 0px; }") 

213 button.setMenu(self.tab_list) 

214 return button 

215 

216 def _tabs_menu(self, where: QPoint): 

217 """The menu for a right-click beside the editor's tabs: View → Editor Tabs.""" 

218 menu = self.tabs_menu = QMenu(self) # (kept while it's open) 

219 for action in self.tab_rows_menu.actions(): # (the same actions: checked alike) 

220 menu.addAction(action) 

221 menu.aboutToHide.connect(menu.deleteLater) 

222 menu.popup(where) 

223 

224 def set_tab_rows(self, wrap: bool, save: bool = True): 

225 """View → Editor Tabs: one row that scrolls, or several rows.""" 

226 self.tab_rows_actions[wrap].setChecked(True) 

227 if save: 

228 self.settings.setValue("tab_rows_wrap", wrap) 

229 self.tabs.tabBar().setVisible(not wrap) 

230 self.tab_list_button.setVisible(not wrap) # (the rows have their own, at their end) 

231 self.tab_rows.setVisible(wrap) 

232 self.tab_rows_list_button.setVisible(wrap) 

233 self.tab_rows.refresh() 

234 

235 def editors(self) -> list[EditorTab]: 

236 return [w for i in range(self.tabs.count()) 

237 if isinstance(w := self.tabs.widget(i), EditorTab)] 

238 

239 def current_editor(self) -> EditorTab | None: 

240 widget = self.tabs.currentWidget() 

241 return widget if isinstance(widget, EditorTab) else None 

242 

243 def _with_editor(self, action): 

244 editor = self.current_editor() 

245 if editor is not None: 245 ↛ exitline 245 didn't return from function '_with_editor' because the condition on line 245 was always true

246 action(editor) 

247 

248 def browse_file(self): 

249 start = str(self.manifest_path.parent) if self.manifest_path else str(Path.home()) 

250 filename, _ = QFileDialog.getOpenFileName(self, "Open file in editor", start) 

251 if filename: 

252 self.open_file(filename, force_editor=True) 

253 

254 def preview_file(self, path: str): 

255 """Project files → one click: show the file in the preview tab (its 

256 name in italics), in place of the one previewed before. Editing it, 

257 or double-clicking the file or the tab, keeps it open.""" 

258 file = Path(path).resolve() 

259 if self.use_editor_action.isChecked() and file.is_file() and EditorTab.is_probably_text(file): 

260 self.open_file(str(file), force_editor=True, preview=True) 

261 

262 def _preview_editor(self) -> EditorTab | None: 

263 return self._preview if self._preview in self.editors() else None 

264 

265 def _keep_open(self, path: str): 

266 """A file opened as a preview, kept open (a result double-clicked).""" 

267 editor = next((e for e in self.editors() if e.path == Path(path).resolve()), None) 

268 if editor is not None: 268 ↛ exitline 268 didn't return from function '_keep_open' because the condition on line 268 was always true

269 self._make_permanent(editor) 

270 

271 def _make_permanent(self, editor): 

272 """A preview tab stays open from now on (its name no longer in italics).""" 

273 if editor is not None and editor is self._preview: 

274 self._preview = None 

275 self._update_editor_title(editor) 

276 

277 def open_file(self, path: str, diagnostics: list | None = None, force_editor: bool = False, 

278 preview: bool = False): 

279 """Open a file in the built-in editor (or the default application), 

280 marking any compiler diagnostics and jumping to the first one. 

281 ``preview``: in the preview tab (see preview_file), and the keyboard 

282 stays where it is.""" 

283 file = Path(path).resolve() 

284 editor = next((e for e in self.editors() if e.path == file), None) # (open already: there, even unsaved) 

285 use_editor = force_editor or self.use_editor_action.isChecked() 

286 if editor is None and (not use_editor or not file.is_file() or not EditorTab.is_probably_text(file)): 286 ↛ 287line 286 didn't jump to line 287 because the condition on line 286 was never true

287 QDesktopServices.openUrl(QUrl.fromLocalFile(str(file))) 

288 return 

289 if editor is not None and not preview: 

290 self._make_permanent(editor) # (opened for real: a preview no longer) 

291 if editor is None: 

292 try: 

293 editor = EditorTab(file) 

294 except OSError as exc: 

295 QMessageBox.warning(self, "Can't open file", f"{file}:\n{exc}") 

296 return 

297 # A new preview takes the old one's place (unless that one has changes: then it stays). 

298 replaced = self._preview_editor() if preview else None 

299 index = self.tabs.indexOf(replaced) if replaced is not None and not replaced.is_dirty() else -1 

300 if preview: 

301 self._preview = editor 

302 self._add_editor(editor, index) 

303 if index >= 0: 

304 self._close_tab(replaced) 

305 self.tabs.setCurrentWidget(editor) 

306 if diagnostics: 

307 editor.show_diagnostics(diagnostics) 

308 editor.editor.go_to_line(diagnostics[0].line) 

309 elif not preview: 

310 editor.editor.setFocus() 

311 

312 def new_file(self): 

313 """File → New File: an empty tab for a file not saved yet ("Untitled-1"…, 

314 in the project's folder when it's saved, unless another is chosen). 

315 Plain text until Save As gives it a name, and with it a file type.""" 

316 folder = self.manifest_path.parent if self.manifest_path else Path.home() 

317 taken = {e.path.name for e in self.editors()} 

318 number = 1 

319 while f"Untitled-{number}" in taken or (folder / f"Untitled-{number}").exists(): 

320 number += 1 

321 editor = EditorTab(folder / f"Untitled-{number}", untitled=True) 

322 self._add_editor(editor) 

323 self.tabs.setCurrentWidget(editor) 

324 editor.editor.setFocus() 

325 

326 def _file_created(self, path: str): 

327 """Save As made a new file: in Project files now.""" 

328 self.project_tree.refresh() 

329 self.project_tree.reveal(path) 

330 

331 def _add_editor(self, editor: EditorTab, index: int = -1): 

332 """A new tab for ``editor`` (at ``index``, or the end), connected to the window.""" 

333 editor.set_font(self._mono_font(editor=True)) 

334 self._show_coverage(editor) 

335 editor.dirty_changed.connect(lambda _dirty, e=editor: self._update_editor_title(e)) 

336 editor.path_changed.connect(lambda _p, e=editor: self._update_editor_title(e)) 

337 editor.path_changed.connect(self._update_editor_actions) 

338 editor.saved.connect(self._editor_saved) 

339 editor.created.connect(self._file_created) 

340 editor.navigate_requested.connect(self.navigate_to) 

341 editor.references_found.connect(self._show_references) 

342 editor.rename_ready.connect(self._preview_rename) 

343 editor.editor.word_added.connect(self.add_spelling_word) 

344 editor.explain_requested.connect(lambda problem, e=editor: self.explain_problem(e, problem)) 

345 editor.editor.edit_selection_requested.connect(lambda e=editor: self.edit_with_ai(e)) 

346 editor.problems_changed.connect(lambda e=editor: self._problems_timer.start() if e is self.current_editor() 

347 else None) 

348 editor.editor.textChanged.connect( # (a new file not saved yet: not in Find in Files) 

349 lambda e=editor: None if e.untitled else self.search_panel.file_changed(e.path)) 

350 editor.attach_lsp(self.lsp_client) 

351 self._debug_editor_added(editor) # (its breakpoints: see window.debugging) 

352 # (Changing a preview's text keeps it open.) 

353 editor.dirty_changed.connect(lambda dirty, e=editor: dirty and self._make_permanent(e)) 

354 if index >= 0: 

355 self.tabs.insertTab(index, editor, editor.path.name) 

356 else: 

357 self.tabs.addTab(editor, editor.path.name) 

358 self._add_close_button(editor) 

359 self._update_editor_title(editor) 

360 self._save_session() 

361 

362 def _close_tab(self, editor: EditorTab): 

363 """Close an editor tab without asking (see close_editor for that).""" 

364 self._debug_editor_closing(editor) # (its breakpoints kept) 

365 editor.close_down() 

366 self.tabs.removeTab(self.tabs.indexOf(editor)) 

367 editor.deleteLater() 

368 if editor is self._preview: 

369 self._preview = None 

370 

371 def _add_close_button(self, editor: EditorTab): 

372 """The file's icon on the left of an editor tab's name, and our own × 

373 on the right (macOS's dark-mode close button is hard, or impossible, 

374 to see). Each side gets one small widget: macOS's style places the 

375 name as if the widget beside it were the usual size, so a wider one 

376 (or a tab icon) covers the start of the name.""" 

377 button = QToolButton() 

378 button.setAutoRaise(True) 

379 button.setFixedSize(18, 18) 

380 button.setIconSize(QSize(12, 12)) 

381 button.setCursor(Qt.ArrowCursor) 

382 keys = self.close_tab_action.shortcut().toString(QKeySequence.NativeText) 

383 button.setToolTip(f"Close ({keys})" if keys else "Close") 

384 button.setStyleSheet("QToolButton { border: none; border-radius: 4px; padding: 0; }" 

385 "QToolButton:hover { background: rgba(128, 128, 128, 80); }") 

386 button.clicked.connect(lambda _=False, e=editor: self.close_editor(self.tabs.indexOf(e))) 

387 editor.close_button = button 

388 self._style_close_button(button) 

389 icon = QLabel() 

390 icon.setFixedSize(16, 16) 

391 editor.tab_icon = icon 

392 self._set_tab_icon(editor) 

393 bar = self.tabs.tabBar() 

394 index = self.tabs.indexOf(editor) 

395 bar.setTabButton(index, QTabBar.LeftSide, icon) 

396 bar.setTabButton(index, QTabBar.RightSide, button) 

397 

398 def _set_tab_icon(self, editor: EditorTab): 

399 """The file's icon, as in Project files (it changes with Save As).""" 

400 label = getattr(editor, "tab_icon", None) 

401 if label is not None: 401 ↛ exitline 401 didn't return from function '_set_tab_icon' because the condition on line 401 was always true

402 label.setPixmap(self.project_tree.icon_for(editor.path).pixmap(16, 16)) 

403 

404 @staticmethod 

405 def _style_close_button(button: QToolButton): 

406 # A mid gray: seen on the dark tabs and on the light selected one 

407 # (macOS's dark mode draws the selected tab light). 

408 button.setIcon(make_icon("close", QColor(140, 140, 140))) 

409 

410 def _fill_tab_list(self, menu: QMenu): 

411 """The tab-list menu: every open tab (the current one ticked).""" 

412 menu.clear() 

413 current = self.tabs.currentIndex() 

414 for index in range(self.tabs.count()): # (every tab is a file open in the editor) 

415 icon = self.project_tree.icon_for(self.tabs.widget(index).path) 

416 action = menu.addAction(icon, self.tabs.tabBar().tab_name(index)) 

417 action.setCheckable(True) 

418 action.setChecked(index == current) 

419 action.setToolTip(self.tabs.tabToolTip(index)) 

420 action.triggered.connect(lambda _checked=False, i=index: self.tabs.setCurrentIndex(i)) 

421 menu.addSeparator() 

422 close_all = menu.addAction("Close All Editor Tabs") 

423 close_all.setToolTip("Close every file open in the editor (unsaved changes are asked about)") 

424 close_all.setEnabled(bool(self.editors())) 

425 close_all.triggered.connect(self.close_all_editor_tabs) 

426 menu.setToolTipsVisible(True) 

427 

428 def close_all_editor_tabs(self): 

429 """The tab list's Close All Editor Tabs (Cancel on an unsaved file 

430 keeps them all open).""" 

431 if self.close_all_editors(): 431 ↛ exitline 431 didn't return from function 'close_all_editor_tabs' because the condition on line 431 was always true

432 self._save_session() # so they don't come back next time 

433 self._update_editor_actions() 

434 

435 def _update_editor_title(self, editor: EditorTab): 

436 index = self.tabs.indexOf(editor) 

437 if index >= 0: 437 ↛ 442line 437 didn't jump to line 442 because the condition on line 437 was always true

438 self.tabs.tabBar().set_tab_name(index, ("• " if editor.is_dirty() else "") + editor.path.name, 

439 italic=editor is self._preview) 

440 self.tabs.setTabToolTip(index, editor.display_path()) 

441 self._set_tab_icon(editor) 

442 self._update_editor_actions() 

443 

444 def _editor_saved(self, path: str): 

445 self.statusBar().showMessage(f"Saved {path}", 4000) 

446 self.refresh_git() 

447 self._complexity_file_saved(path) 

448 if self.manifest_path and Path(path) == self.manifest_path: 448 ↛ 449line 448 didn't jump to line 449 because the condition on line 448 was never true

449 self.reload() # graph, features and profiles may have changed 

450 elif self.config_path is not None and Path(path).resolve() == self.config_path.resolve(): 

451 self.reload_configurations() # (the build configurations, edited as text) 

452 

453 def rename_file(self, path: str): 

454 """Project files → Rename…: rename a file on disk; a tab it's open in 

455 follows it (unsaved changes included).""" 

456 old = Path(path).resolve() 

457 name, ok = QInputDialog.getText(self, "Rename", f"New name for {old.name}:", text=old.name) 

458 name = name.strip() if ok else "" 

459 if not name or name == old.name: 

460 return 

461 if "/" in name or "\\" in name or name in (".", ".."): 

462 QMessageBox.warning(self, "Rename", f"“{name}” isn't a file name (it can't contain / or \\).") 

463 return 

464 new = old.with_name(name) 

465 if new.exists() and not os.path.samefile(new, old): # (another file; a change of case is fine) 

466 QMessageBox.warning(self, "Rename", f"There's already a file named {name} in {old.parent.name}.") 

467 return 

468 try: 

469 old.rename(new) 

470 except OSError as exc: 

471 QMessageBox.warning(self, "Rename", f"Couldn't rename {old.name}:\n{exc}") 

472 return 

473 editor = self._open_editor_for(old) 

474 if editor is not None: 

475 editor.renamed(new) 

476 self._save_session() 

477 self._lsp_diagnostics_by_file.pop(old, None) 

478 self.search_panel.file_changed(old) 

479 self.search_panel.file_changed(new) 

480 self.project_tree.refresh() 

481 self.project_tree.reveal(str(new)) 

482 self.statusBar().showMessage(f"Renamed {old.name} to {name}", 4000) 

483 

484 def delete_file(self, path: str): 

485 """Project files → Move to Trash…: after asking (and, for a file open 

486 with unsaved changes, whether to save them first), move a file to the 

487 trash and close its tab.""" 

488 file = Path(path).resolve() 

489 editor = self._open_editor_for(file) 

490 if editor is not None and editor.is_dirty(): 

491 self.tabs.setCurrentWidget(editor) 

492 answer = QMessageBox.question( 

493 self, "Move to Trash", 

494 f"{file.name} has unsaved changes. Save them before moving it to the trash?", 

495 QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Cancel) 

496 if answer == QMessageBox.Cancel or (answer == QMessageBox.Save and not editor.save()): 

497 return 

498 else: 

499 answer = QMessageBox.question(self, "Move to Trash", f"Move {file.name} to the trash?", 

500 QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) 

501 if answer != QMessageBox.Yes: 

502 return 

503 moved = QFile.moveToTrash(str(file)) 

504 moved = moved[0] if isinstance(moved, tuple) else moved # (PySide: (ok, path in the trash)) 

505 if not moved: 

506 QMessageBox.warning(self, "Move to Trash", f"Couldn't move {file.name} to the trash.") 

507 return 

508 if editor is not None: 508 ↛ 511line 508 didn't jump to line 511 because the condition on line 508 was always true

509 self._close_tab(editor) 

510 self._save_session() 

511 self._lsp_diagnostics_by_file.pop(file, None) 

512 self.search_panel.file_changed(file) 

513 self.project_tree.refresh() 

514 self.statusBar().showMessage(f"Moved {file.name} to the trash", 4000) 

515 

516 def close_all_editors(self) -> bool: 

517 """Close every editor tab. Unsaved files are asked about first; if 

518 any answer is Cancel, nothing is closed and False is returned.""" 

519 editors = self.editors() 

520 for editor in editors: 

521 if editor.is_dirty(): 

522 self.tabs.setCurrentWidget(editor) 

523 if not editor.maybe_save(): 523 ↛ 520line 523 didn't jump to line 520 because the condition on line 523 was always true

524 return False 

525 for editor in editors: 

526 self._close_tab(editor) 

527 return True 

528 

529 def close_editor(self, index: int): 

530 widget = self.tabs.widget(index) 

531 if not isinstance(widget, EditorTab): 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true

532 return 

533 self.tabs.setCurrentIndex(index) 

534 if widget.maybe_save(): 

535 self._close_tab(widget) 

536 self._save_session() 

537 

538 def _location(self) -> tuple[str, int, int] | None: 

539 editor = self.current_editor() 

540 if editor is None: 

541 return None 

542 cursor = editor.editor.textCursor() 

543 return str(editor.path), cursor.blockNumber() + 1, cursor.positionInBlock() 

544 

545 def navigate_to(self, path: str, line: int, column: int, record: bool = True, preview: bool = False): 

546 """Open a file at a line and column (Go to Definition / Symbol); 

547 Go Back returns to where this was called from. ``preview``: in the 

548 preview tab, if it isn't open already (see preview_file).""" 

549 here = self._location() 

550 if record and here is not None and here != (path, line, column): 

551 self._back.append(here) 

552 del self._back[:-100] 

553 self._forward.clear() 

554 self.open_file(path, force_editor=True, preview=preview) 

555 editor = self.current_editor() 

556 if editor is not None and editor.path == Path(path).resolve(): 556 ↛ 559line 556 didn't jump to line 559 because the condition on line 556 was always true

557 editor.editor.go_to_position(line, column) 

558 editor.editor.setFocus() 

559 self._update_editor_actions() 

560 

561 def _go_history(self, back: bool): 

562 source, target = (self._back, self._forward) if back else (self._forward, self._back) 

563 while source: 563 ↛ 571line 563 didn't jump to line 571 because the condition on line 563 was always true

564 path, line, column = source.pop() 

565 if Path(path).is_file(): 565 ↛ 563line 565 didn't jump to line 563 because the condition on line 565 was always true

566 here = self._location() 

567 if here is not None: 567 ↛ 569line 567 didn't jump to line 569 because the condition on line 567 was always true

568 target.append(here) 

569 self.navigate_to(path, line, column, record=False) 

570 return 

571 self._update_editor_actions()