diff --git a/bouquin/code_block_editor_dialog.py b/bouquin/code_block_editor_dialog.py index a5e3832..7b7b1b0 100644 --- a/bouquin/code_block_editor_dialog.py +++ b/bouquin/code_block_editor_dialog.py @@ -13,7 +13,9 @@ from . import strings class CodeBlockEditorDialog(QDialog): - def __init__(self, code: str, language: str | None, parent=None): + def __init__( + self, code: str, language: str | None, parent=None, allow_delete: bool = False + ): super().__init__(parent) self.setWindowTitle(strings._("edit_code_block")) @@ -21,6 +23,9 @@ class CodeBlockEditorDialog(QDialog): self._code_edit = QPlainTextEdit(self) self._code_edit.setPlainText(code) + # Track whether the user clicked "Delete" + self._delete_requested = False + # Language selector (optional) self._lang_combo = QComboBox(self) languages = [ @@ -44,12 +49,28 @@ class CodeBlockEditorDialog(QDialog): buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) + if allow_delete: + delete_btn = buttons.addButton( + strings._("delete_code_block"), + QDialogButtonBox.ButtonRole.DestructiveRole, + ) + delete_btn.clicked.connect(self._on_delete_clicked) + layout = QVBoxLayout(self) layout.addWidget(QLabel(strings._("locale") + ":", self)) layout.addWidget(self._lang_combo) layout.addWidget(self._code_edit) layout.addWidget(buttons) + def _on_delete_clicked(self) -> None: + """Mark this dialog as 'delete requested' and close as Accepted.""" + self._delete_requested = True + self.accept() + + def was_deleted(self) -> bool: + """Return True if the user chose to delete the code block.""" + return self._delete_requested + def code(self) -> str: return self._code_edit.toPlainText() diff --git a/bouquin/code_highlighter.py b/bouquin/code_highlighter.py index 2689b9d..a56438a 100644 --- a/bouquin/code_highlighter.py +++ b/bouquin/code_highlighter.py @@ -365,3 +365,7 @@ class CodeBlockMetadata: self._block_languages[int(block_num)] = lang except ValueError: pass + + def clear_language(self, block_number: int): + """Remove any stored language for a given block, if present.""" + self._block_languages.pop(block_number, None) diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index f6149d8..1c97dba 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -290,5 +290,6 @@ "saturday": "Saturday", "sunday": "Sunday", "day": "Day", - "edit_code_block": "Edit code block" + "edit_code_block": "Edit code block", + "delete_code_block": "Delete code block" } diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index 490a595..9e7bca6 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -372,12 +372,17 @@ class MarkdownEditor(QTextEdit): code_text = self._get_code_block_text(open_block, close_block) - dlg = CodeBlockEditorDialog(code_text, lang, parent=self) + dlg = CodeBlockEditorDialog(code_text, lang, parent=self, allow_delete=True) result = dlg.exec() if result != QDialog.DialogCode.Accepted: # Dialog was shown but user cancelled; event is "handled". return True + # If the user requested deletion, remove the whole block + if hasattr(dlg, "was_deleted") and dlg.was_deleted(): + self._delete_code_block(open_block) + return True + new_code = dlg.code() new_lang = dlg.language() @@ -396,6 +401,58 @@ class MarkdownEditor(QTextEdit): return True + def _delete_code_block(self, block: QTextBlock) -> bool: + """Delete the fenced code block containing `block`. + + Returns True if a block was deleted, False otherwise. + """ + bounds = self._find_code_block_bounds(block) + if not bounds: + return False + + open_block, close_block = bounds + doc = self.document() + if doc is None: + return False + + # Remove from the opening fence down to just before the block after + # the closing fence (so we also remove the trailing blank line). + start_pos = open_block.position() + after_block = close_block.next() + if after_block.isValid(): + end_pos = after_block.position() + else: + end_pos = close_block.position() + len(close_block.text()) + + cursor = QTextCursor(doc) + cursor.beginEditBlock() + cursor.setPosition(start_pos) + cursor.setPosition(end_pos, QTextCursor.MoveMode.KeepAnchor) + cursor.removeSelectedText() + cursor.endEditBlock() + + # Clear language metadata for this block, if supported + if hasattr(self, "_code_metadata"): + clear = getattr(self._code_metadata, "clear_language", None) + if clear is not None: + clear(open_block.blockNumber()) + + # Refresh visuals (spacing + backgrounds + syntax) + if hasattr(self, "_apply_code_block_spacing"): + self._apply_code_block_spacing() + if hasattr(self, "_update_code_block_row_backgrounds"): + self._update_code_block_row_backgrounds() + if hasattr(self, "highlighter"): + self.highlighter.rehighlight() + + # Move caret to where the block used to be + cursor = self.textCursor() + cursor.setPosition(start_pos) + self.setTextCursor(cursor) + self.setFocus() + + return True + def _apply_line_spacing(self, height: float = 125.0): """Apply proportional line spacing to the whole document.""" doc = self.document() @@ -1619,6 +1676,10 @@ class MarkdownEditor(QTextEdit): edit_action.triggered.connect(lambda: self._edit_code_block(block)) menu.addAction(edit_action) + delete_action = QAction(strings._("delete_code_block"), self) + delete_action.triggered.connect(lambda: self._delete_code_block(block)) + menu.addAction(delete_action) + menu.addSeparator() # Add standard context menu actions