"""Code Editor view — renders the Ace editor, file tabs, and execution output.""" import ast import streamlit as st import streamlit_ace as st_ace from pathlib import Path from backend.managers.file_manager import FileManager from backend.managers.execution_engine import ExecutionEngine from backend.managers.debug_logger import get_logger logger = get_logger(__name__) # Maps file extensions to Ace editor language modes for syntax highlighting. LANG_MAP = { ".py": "python", ".tex": "latex", ".js": "javascript", ".html": "html", ".css": "css", ".sh": "bash", ".json": "json", ".yaml": "yaml", ".yml": "yaml" } # ── Modals ──────────────────────────────────────────────────────────────────── @st.dialog("Rename File") def _rename_dialog(file_path: str): """Dialog for renaming the given file. Updates open_files, files_content and active_file in session_state so all tabs and the editor reference the new path immediately. """ fm = FileManager() st.write(f"Current name: **{Path(file_path).name}**") new_name = st.text_input("New name:", value=Path(file_path).stem) col1, col2 = st.columns(2) with col1: if st.button("Confirm", type="primary", use_container_width=True): if not new_name.strip(): st.warning("Please enter a name.") elif "/" in new_name or "\\" in new_name: st.warning("Name must not contain slashes.") else: rel_path = str(Path(file_path).relative_to(fm.base_path)) if fm.rename_file(rel_path, new_name.strip()): ext = Path(file_path).suffix new_file_path = str(Path(file_path).parent / (Path(new_name.strip()).stem + ext)) # Update the open-files list in place so the tab order is preserved. i = st.session_state.open_files.index(file_path) st.session_state.open_files[i] = new_file_path # Transfer cached editor content to the new path key. st.session_state.files_content[new_file_path] = \ st.session_state.files_content.pop(file_path) # Update active_file if the renamed file was the active one. if st.session_state.active_file == file_path: st.session_state.active_file = new_file_path st.rerun() logger.info("Rename file %s to %s successfull", file_path, new_name ) else: logger.warning("Rename failed.") st.error("Rename failed. Check that the file %s still exists.", file_path) with col2: if st.button("Cancel", use_container_width=True): st.rerun() @st.dialog("Delete File") def _delete_dialog(abs_file_path: str): """Confirmation dialog before permanently deleting the given file. Removes the file from disk and also cleans up the editor's open-files list, content cache, and active_file pointer so the UI stays consistent. Args: abs_file_path: Absolute path to the file that should be deleted. """ fm = FileManager() file_name = Path(abs_file_path).name relative_path = str(Path(abs_file_path).relative_to(fm.base_path)) st.warning(f"Delete **{file_name}**? This cannot be undone.") col1, col2 = st.columns(2) with col1: if st.button("Delete", type="primary", use_container_width=True): if fm.delete_file(relative_path): st.session_state.open_files.remove(abs_file_path) st.session_state.files_content.pop(abs_file_path, None) if st.session_state.active_file == abs_file_path: st.session_state.active_file = ( st.session_state.open_files[0] if st.session_state.open_files else None ) st.rerun() logger.info("Deleting file %s successfull.", abs_file_path) else: st.error("Delete failed. Check that the file still exists.") logger.warning("Deleting file %s failed.", abs_file_path) with col2: if st.button("Cancel", use_container_width=True): st.rerun() def run_active_file(): """Execute the currently active file and store the result in exec_results[file_path]. Runs an ast.parse() check first — if the syntax is invalid the file is not executed and ast_error=True is stored so the UI can show a targeted warning. Returns: The result dict, or None if no active file is set. """ active_file = st.session_state.active_file if not active_file: st.warning("No active file to run.") return execution_engine = ExecutionEngine() logger.info("Executing code from %s...", active_file) # ast check — only for Python files if Path(active_file).suffix == ".py": source = st.session_state.get("files_content", {}).get(active_file, "") try: ast.parse(source) except SyntaxError as e: result = {"stdout": "", "stderr": str(e), "return_code": -1, "ast_error": True} st.session_state.exec_results[active_file] = result debug_logger.log_error(f"Syntax error: {e}") return result with st.spinner(f"Running {Path(active_file).name}..."): output = execution_engine.run_code(Path(active_file)) if output["rc"] == 0: logger.info("Execution completed successfully.") else: logger.error("Execution failed with exit code %s.", output['rc']) result = { "stdout": output["stdout"], "stderr": output["stderr"], "return_code": output["rc"], "ast_error": False, } st.session_state.exec_results[active_file] = result return result def render_editor(): """Render the full Code Editor view with tabs, Ace editor, and run output.""" st.subheader("Code Editor") if not st.session_state.open_files: st.info("Please select a file to edit.") return fm = FileManager() # ── Tab bar via st.tabs() ───────────────────────────────────────────────── # Build one tab per open file, named by the file's basename. tab_names = [Path(f).name for f in st.session_state.open_files] tabs = st.tabs(tab_names) # Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun. # Wenn _jump_to_tab gesetzt ist, klickt das Script den richtigen Tab an. jump_target = st.session_state.pop("_jump_to_tab", None) if jump_target and jump_target in st.session_state.open_files: idx = st.session_state.open_files.index(jump_target) st.components.v1.html( f"""""", height=0, ) for idx, file_path in enumerate(st.session_state.open_files): with tabs[idx]: # Load file content from disk on first open; afterwards use the cached version. if file_path not in st.session_state.files_content: st.session_state.files_content[file_path] = fm.read_file(Path(file_path)) file_language = LANG_MAP.get(Path(file_path).suffix, "text") # Ace editor widget — auto_update sends content to Python on each keystroke. code = st_ace.st_ace( value=st.session_state.files_content[file_path], language=file_language, theme="monokai", key=f"code_editor_{file_path}", auto_update=True, height=400, ) # Keep the in-memory cache in sync with what the editor currently shows. # REVIEW: redundant round-trip — st_ace returns the same value that was passed as # `value=` unless the user edited the content; comparing and re-assigning on every # rerun is a no-op most of the time and adds overhead. if code != st.session_state.files_content[file_path]: st.session_state.files_content[file_path] = code cols = st.columns([1, 1, 1, 1]) with cols[0]: if st.button("Save Changes", key=f"save_{file_path}"): if fm.save_file(file_path, code): st.success("File saved successfully!") with cols[1]: if st.button("Close File", key=f"close_{file_path}"): st.session_state.open_files.remove(file_path) st.session_state.files_content.pop(file_path, None) # Switch active_file to the next available tab. st.session_state.active_file = ( st.session_state.open_files[0] if st.session_state.open_files else None ) st.rerun() with cols[2]: if st.button("Rename File", key=f"rename_{file_path}"): _rename_dialog(file_path) with cols[3]: if st.button("Delete File", key=f"delete_{file_path}"): _delete_dialog(file_path) # ── Run + Output ────────────────────────────────────────────────── if st.button("▶ Run Code", key=f"run_code_{file_path}", type="primary"): run_active_file() st.rerun() result = st.session_state.get("exec_results", {}).get(file_path) if result: st.subheader("Execution Output") if result.get("ast_error"): st.warning("⚠️ Syntax Error detected before execution — code was not run.") elif result["return_code"] == 0: st.success(f"✅ Exit code: 0") else: st.error(f"❌ Exit code: {result['return_code']}") # Debug with AI — only shown when there is an error or stderr output. if result["return_code"] != 0 or result.get("stderr"): if st.button("🐛 Debug with AI", key=f"debug_with_ai_{file_path}", type="primary"): file_name = Path(file_path).name error_text = result.get("stderr", "") or f"Exit code: {result['return_code']}" code_content = st.session_state.files_content.get(file_path, "") lang = LANG_MAP.get(Path(file_path).suffix, "python") debug_message = ( f"I got an error while running **{file_name}**:\n\n" f"**Error:** {error_text.strip()}\n" f"**Exit Code:** {result['return_code']}\n\n" f"**Here is the code:**\n```{lang}\n{code_content}\n```\n\n" f"Can you help me fix this?" ) st.session_state.pending_debug_message = debug_message st.session_state["_navigate_to_chat"] = True st.rerun() if result.get("stdout"): st.text_area("Standard Output", value=result["stdout"], height=200, disabled=True, key=f"run_stdout_{file_path}") if result.get("stderr"): st.text_area("Standard Error", value=result["stderr"], height=200, disabled=True, key=f"run_stderr_{file_path}") if not result.get("stdout") and not result.get("stderr"): st.info("No output produced by the code execution.") if __name__ == "__main__": render_editor()