import sys import json import os import logging from pathlib import Path from typing import Optional, Dict, Any, List, Tuple from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QLineEdit, QComboBox, QTextEdit, QPushButton, QStatusBar, QMessageBox, QDialog, QListWidget, QSplitter, QFrame, QSizePolicy, QScrollArea, QFileDialog, QToolTip ) from PyQt6.QtCore import Qt, QSize, pyqtSignal, QTimer from PyQt6.QtGui import QFont, QTextOption, QSyntaxHighlighter, QTextCharFormat, QColor, QTextCursor, QKeyEvent, QTextDocument from PyQt6.QtWebEngineWidgets import QWebEngineView # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("ProblemCreator") # Optional imports with proper error handling OPTIONAL_DEPENDENCIES = {} try: import markdown OPTIONAL_DEPENDENCIES['markdown'] = True except ImportError: logger.warning("Markdown library not available, using basic conversion") OPTIONAL_DEPENDENCIES['markdown'] = False try: from pygments import lex from pygments.lexers import PythonLexer from pygments.styles import get_style_by_name OPTIONAL_DEPENDENCIES['pygments'] = True except ImportError: logger.warning("Pygments not available, syntax highlighting will be disabled") OPTIONAL_DEPENDENCIES['pygments'] = False class CodeEditor(QTextEdit): """A code editor with syntax highlighting and advanced auto-indentation.""" def __init__(self, parent=None): super().__init__(parent) self.setFont(QFont("Consolas", 10)) self.indent_width = 4 self.setTabStopDistance(QFont("Consolas", 10).pointSize() * self.indent_width) # Setup syntax highlighting if available if OPTIONAL_DEPENDENCIES.get('pygments', False): self.highlighter = PythonHighlighter(self.document()) # Tips for test case editing self.tips = [ "💡 Tip: Use descriptive test method names like test_empty_input()", "💡 Tip: Test edge cases like empty inputs, large inputs, and invalid inputs", "💡 Tip: Use assertEqual for exact matches, assertTrue/False for boolean checks", "💡 Tip: Test both valid and invalid inputs to ensure robustness", "💡 Tip: Consider using setUp() method for common test setup", "💡 Tip: Use parameterized tests if you have many similar test cases", "💡 Tip: Make sure your tests are independent of each other", "💡 Tip: Test not only for correct outputs but also for proper error handling" ] self.current_tip_index = 0 self.tip_timer = QTimer(self) self.tip_timer.timeout.connect(self.show_next_tip) self.tip_timer.start(10000) # Show a new tip every 10 seconds def show_next_tip(self): """Show the next tip in the status bar.""" if self.parent() and hasattr(self.parent().parent().parent().parent(), 'statusBar'): status_bar = self.parent().parent().parent().parent().statusBar() if status_bar: tip = self.tips[self.current_tip_index] status_bar.showMessage(tip) self.current_tip_index = (self.current_tip_index + 1) % len(self.tips) def keyPressEvent(self, event: QKeyEvent): """Handle key press events for auto-indentation and pairing.""" key = event.key() modifiers = event.modifiers() cursor = self.textCursor() # Tab key if key == Qt.Key.Key_Tab: if cursor.hasSelection(): self.indentSelection() else: # Insert spaces cursor.insertText(" " * self.indent_width) return # Shift+Tab key elif key == Qt.Key.Key_Backtab: if cursor.hasSelection(): self.dedentSelection() else: self.dedentLine() return # Return key elif key == Qt.Key.Key_Return: # Get current line cursor.movePosition(QTextCursor.MoveOperation.StartOfLine) cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor) line_text = cursor.selectedText() # Calculate indentation indent = len(line_text) - len(line_text.lstrip()) # Check if line ends with colon ends_with_colon = line_text.rstrip().endswith(':') # Insert newline with indentation cursor = self.textCursor() cursor.insertText("\n" + " " * indent) # Add extra indentation if line ended with colon if ends_with_colon: cursor.insertText(" " * self.indent_width) return # Auto-pairing elif key == Qt.Key.Key_ParenLeft: cursor.insertText("()") cursor.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(cursor) return elif key == Qt.Key.Key_BracketLeft: cursor.insertText("[]") cursor.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(cursor) return elif key == Qt.Key.Key_BraceLeft: cursor.insertText("{}") cursor.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(cursor) return elif key == Qt.Key.Key_QuoteDbl: cursor.insertText('""') cursor.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(cursor) return elif key == Qt.Key.Key_Apostrophe: cursor.insertText("''") cursor.movePosition(QTextCursor.MoveOperation.Left) self.setTextCursor(cursor) return elif key == Qt.Key.Key_Colon and modifiers == Qt.KeyboardModifier.NoModifier: # Check if we're at the end of the line cursor.movePosition(QTextCursor.MoveOperation.EndOfLine) if self.textCursor().position() == cursor.position(): cursor.insertText(":") return # Default behavior super().keyPressEvent(event) def indentSelection(self): """Indent all selected lines.""" cursor = self.textCursor() start = cursor.selectionStart() end = cursor.selectionEnd() # Move to start of selection cursor.setPosition(start) cursor.movePosition(QTextCursor.MoveOperation.StartOfLine) # Indent each line in selection while cursor.position() <= end: cursor.insertText(" " * self.indent_width) end += self.indent_width if not cursor.movePosition(QTextCursor.MoveOperation.Down): break # Restore selection cursor.setPosition(start) cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) def dedentSelection(self): """Dedent all selected lines.""" cursor = self.textCursor() start = cursor.selectionStart() end = cursor.selectionEnd() # Move to start of selection cursor.setPosition(start) cursor.movePosition(QTextCursor.MoveOperation.StartOfLine) # Dedent each line in selection while cursor.position() <= end: # Check for spaces at beginning of line line_start = cursor.position() cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor) line_text = cursor.selectedText() # Count leading spaces leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width) if leading_spaces > 0: # Remove leading spaces cursor.setPosition(line_start) cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces) cursor.removeSelectedText() end -= leading_spaces if not cursor.movePosition(QTextCursor.MoveOperation.Down): break # Restore selection cursor.setPosition(max(0, start - self.indent_width)) cursor.setPosition(max(0, end - self.indent_width), QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) def dedentLine(self): """Dedent the current line.""" cursor = self.textCursor() cursor.movePosition(QTextCursor.MoveOperation.StartOfLine) # Check for spaces at beginning of line line_start = cursor.position() cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor) line_text = cursor.selectedText() # Count leading spaces leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width) if leading_spaces > 0: # Remove leading spaces cursor.setPosition(line_start) cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces) cursor.removeSelectedText() class PythonHighlighter(QSyntaxHighlighter): """Syntax highlighter for Python code using Pygments.""" def __init__(self, document): super().__init__(document) self._setup_formats() def _setup_formats(self): """Setup text formats for different token types.""" self.formats = {} # Define syntax highlighting formats keyword_format = QTextCharFormat() keyword_format.setForeground(QColor("#0000FF")) keyword_format.setFontWeight(QFont.Weight.Bold) self.formats['keyword'] = keyword_format string_format = QTextCharFormat() string_format.setForeground(QColor("#008000")) self.formats['string'] = string_format comment_format = QTextCharFormat() comment_format.setForeground(QColor("#808080")) comment_format.setFontItalic(True) self.formats['comment'] = comment_format function_format = QTextCharFormat() function_format.setForeground(QColor("#000080")) function_format.setFontWeight(QFont.Weight.Bold) self.formats['function'] = function_format number_format = QTextCharFormat() number_format.setForeground(QColor("#FF8C00")) self.formats['number'] = number_format # Python keywords self.keywords = [ 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield' ] # unittest keywords self.unittest_keywords = [ 'TestCase', 'setUp', 'tearDown', 'setUpClass', 'tearDownClass', 'assertEqual', 'assertTrue', 'assertFalse', 'assertRaises', 'assertAlmostEqual', 'assertNotEqual', 'assertIn', 'assertNotIn', 'assertIs', 'assertIsNot', 'assertIsNone', 'assertIsNotNone', 'assertIsInstance', 'assertNotIsInstance', 'assertDictEqual', 'assertListEqual', 'assertTupleEqual', 'assertSetEqual', 'assertSequenceEqual', 'assertMultiLineEqual', 'assertGreater', 'assertGreaterEqual', 'assertLess', 'assertLessEqual', 'assertRegex', 'assertNotRegex', 'assertCountEqual' ] def highlightBlock(self, text): """Apply syntax highlighting to the current text block.""" # Check if we should use pygments if OPTIONAL_DEPENDENCIES.get('pygments', False): self._highlight_with_pygments(text) else: self._highlight_with_basic_rules(text) def _highlight_with_pygments(self, text): """Use pygments for syntax highlighting if available.""" try: # Get the text from the current block block = self.currentBlock() start_pos = block.position() end_pos = start_pos + len(text) full_text = self.document().toPlainText() # Lex the code and apply formats for token, value in lex(full_text, PythonLexer()): token_str = str(token) token_start = full_text.find(value, start_pos) # Skip if token is not in current block if token_start < start_pos or token_start >= end_pos: continue # Calculate length within current block token_len = min(len(value), end_pos - token_start) # Apply appropriate format if 'Keyword' in token_str: self.setFormat(token_start - start_pos, token_len, self.formats['keyword']) elif 'String' in token_str: self.setFormat(token_start - start_pos, token_len, self.formats['string']) elif 'Comment' in token_str: self.setFormat(token_start - start_pos, token_len, self.formats['comment']) elif 'Name' in token_str and 'Function' in token_str: self.setFormat(token_start - start_pos, token_len, self.formats['function']) elif 'Number' in token_str: self.setFormat(token_start - start_pos, token_len, self.formats['number']) except Exception as e: logger.error(f"Error during pygments highlighting: {e}") # Fall back to basic highlighting self._highlight_with_basic_rules(text) def _highlight_with_basic_rules(self, text): """Use basic rules for syntax highlighting.""" # Highlight keywords for keyword in self.keywords + self.unittest_keywords: pattern = r'\b' + keyword + r'\b' index = 0 while index < len(text): index = text.find(keyword, index) if index == -1: break # Check if it's really a word (not part of another word) if (index == 0 or not text[index-1].isalnum()) and \ (index + len(keyword) >= len(text) or not text[index + len(keyword)].isalnum()): if keyword in self.keywords: self.setFormat(index, len(keyword), self.formats['keyword']) else: self.setFormat(index, len(keyword), self.formats['function']) index += len(keyword) # Highlight strings import re string_pattern = re.compile(r'(\".*?\")|(\'.*?\')') for match in string_pattern.finditer(text): start, end = match.span() self.setFormat(start, end - start, self.formats['string']) # Highlight comments comment_pattern = re.compile(r'#.*') for match in comment_pattern.finditer(text): start, end = match.span() self.setFormat(start, end - start, self.formats['comment']) # Highlight numbers number_pattern = re.compile(r'\b\d+\b') for match in number_pattern.finditer(text): start, end = match.span() self.setFormat(start, end - start, self.formats['number']) class MarkdownEditor(QWidget): """A markdown editor with live preview.""" def __init__(self, parent=None): super().__init__(parent) # Create split view self.splitter = QSplitter(Qt.Orientation.Horizontal) layout = QVBoxLayout(self) layout.addWidget(self.splitter) # Left side - text editor self.editor = QTextEdit() self.editor.setFont(QFont("Consolas", 10)) self.editor.textChanged.connect(self.update_preview) self.splitter.addWidget(self.editor) # Right side - preview self.preview = QWebEngineView() self.splitter.addWidget(self.preview) # Set initial sizes self.splitter.setSizes([400, 400]) def update_preview(self): """Update the markdown preview.""" # Get the markdown text markdown_text = self.editor.toPlainText() # Convert to HTML html_content = self._markdown_to_html(markdown_text) # Update the preview self.preview.setHtml(html_content) def _markdown_to_html(self, text): """Convert markdown text to HTML.""" if OPTIONAL_DEPENDENCIES.get('markdown', False): # Use the markdown library if available html = markdown.markdown(text) else: # Fallback to basic conversion html = text html = html.replace("# ", "
").replace("`", "")
html = html.replace("\n", "