rename and delet options per file in File Tree/sidebar

This commit is contained in:
Irina Rueegg 2026-05-05 19:38:04 +02:00
parent c00a2454fc
commit 73ba4a7931
4 changed files with 232 additions and 27 deletions

View File

@ -10,6 +10,17 @@ class FileManager:
self.base_path.mkdir(exist_ok=True)
def create_folder(self, relative_path: str, name: str) -> bool:
"""
Creates a new folder at the specified relative path.
The relative_path should be the path to the folder relative to the base path,
and name should be the name of the new folder (without any slashes).
Args:
relative_path (str): The relative path (without base path) where the new folder should be created.
name (str): The name of the new folder to create (should not contain slashes).
Returns:
bool: True if folder was created successfully, False otherwise.
"""
if not name:
st.error(f"Invalid folder name: {name}")
return False
@ -40,7 +51,18 @@ class FileManager:
st.error(f"Error creating folder {relative_path}: {str(e)}")
return False
def create_file(self, relative_path: str, name: str) -> bool:
def create_file(self, relative_path: str, name: str) -> bool:
"""
Creates a new file at the specified relative path.
The relative_path should be the path to the folder relative to the base path where the file should be created,
and name should be the name of the new file (without any slashes).
Args:
relative_path (str): The relative path (without base path) where the new file should be created.
name (str): The name of the new file to create (should not contain slashes).
Returns:
bool: True if file was created successfully, False otherwise.
"""
if not name or name.strip() == "" :
st.error(f"Invalid file name: {name}")
return False
@ -71,6 +93,15 @@ class FileManager:
return False
def read_file(self, relative_path: Path) -> str:
"""
Reads the content of a file.
The relative_path should be the path to the file relative to the base path.
Args:
relative_path (str): The relative path (without base path) to the file to read, including the file name
Returns:
str: The content of the file, or an empty string if there was an error.
"""
file_path = (relative_path).resolve()
if not file_path.exists():
@ -93,7 +124,17 @@ class FileManager:
st.error(f"Error reading file {relative_path}: {str(e)}")
return ""
def save_file(self, relative_path: str, content: str):
def save_file(self, relative_path: str, content: str) -> bool:
"""
Saves content to a file.
The relative_path should be the path to the file relative to the base path.
Args:
relative_path (str): The relative path(without base path) to the file to save, including the file name
content (str): The content to write to the file
Returns:
bool: True if save was successful, False otherwise.
"""
file_path = (Path(relative_path)).resolve()
if not str(file_path).startswith(str(self.base_path.resolve())):
@ -109,6 +150,16 @@ class FileManager:
return False
def rename_file(self, old_relative_path: str, new_name: str) -> bool:
"""
Renames a file while keeping the same extension.
The new_name should not include the extension, as it will be preserved from the old name.
Args:
old_relative_path (str): The current relative path (without base path) of the file to rename, including the file name.
new_name (str): The new name for the file, without extension.
Returns:
bool: True if rename was successful, False otherwise.
"""
if not new_name or new_name.strip() == "":
st.error(f"Invalid file name: {new_name}")
return False
@ -119,7 +170,7 @@ class FileManager:
if not Path(new_name).suffix == file_type:
new_name = Path(new_name).with_suffix(file_type) # Ensure the file extension remains the same
old_file_path = (Path(old_relative_path)).resolve()
old_file_path = (Path(self.base_path / old_relative_path)).resolve()
new_file_path = old_file_path.parent / new_name
if not str(old_file_path).startswith(str(self.base_path.resolve())) or not str(new_file_path).startswith(str(self.base_path.resolve())):
@ -131,9 +182,18 @@ class FileManager:
return True
except FileNotFoundError:
st.error(f"File not found: {old_relative_path}")
return False
return False
def delete_folder(self, relative_path) -> bool:
def delete_folder(self, relative_path: str) -> bool:
"""Deletes a folder and all its contents.
The relative_path should be the path to the folder relative to the base path.
Args:
relative_path (str): The relative path (without base path) to the folder to delete.
Returns:
bool: True if deletion was successful, False otherwise.
"""
folder_path = (self.base_path / relative_path).resolve()
if not str(folder_path).startswith(str(self.base_path.resolve())):
@ -152,15 +212,25 @@ class FileManager:
st.error(f"Error deleting folder {relative_path}: {str(e)}")
return False
def delete_file(self, relative_path):
file_path = Path(relative_path).resolve()
def delete_file(self, relative_path: str) -> bool:
"""Deletes a file.
The relative_path should be the path to the file relative to the base path.
Args:
relative_path (str): The relative path (without base path) to the file to delete, including the file name.
Returns:
bool: True if deletion was successful, False otherwise.
"""
file_path = Path(relative_path)
abs_file_path = (Path(self.base_path) / file_path).resolve()
print(f"Absolute file path resolved to: {abs_file_path}") # Debugging info
if not str(file_path).startswith(str(self.base_path.resolve())):
if not str(abs_file_path).startswith(str(self.base_path.resolve())):
st.error(f"Access denied: {relative_path}")
return False
try:
file_path.unlink()
abs_file_path.unlink()
return True
except FileNotFoundError:
st.error(f"File not found: {relative_path}")
@ -170,6 +240,14 @@ class FileManager:
return False
def get_file_tree(self):
"""
Builds a nested dictionary representing the file tree starting from the base path.
Directories are represented as keys with dictionary values,
and files are represented as keys with None
Returns:
dict: A nested dictionary representing the file tree.
"""
def build_tree(path: Path):
tree = {}

View File

@ -13,6 +13,7 @@ LANG_MAP = {
}
# ── Modals ────────────────────────────────────────────────────────────────────
@st.dialog("Rename File")
@ -29,7 +30,8 @@ def _rename_dialog(file_path: str):
elif "/" in new_name or "\\" in new_name:
st.warning("Name must not contain slashes.")
else:
if fm.rename_file(file_path, new_name.strip()):
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))
i = st.session_state.open_files.index(file_path)
@ -47,17 +49,19 @@ def _rename_dialog(file_path: str):
@st.dialog("Delete File")
def _delete_dialog(file_path: str):
def _delete_dialog(abs_file_path: str):
fm = FileManager()
st.warning(f"Delete **{Path(file_path).name}**? This cannot be undone.")
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(file_path):
st.session_state.open_files.remove(file_path)
st.session_state.files_content.pop(file_path, None)
if st.session_state.active_file == file_path:
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

View File

@ -46,10 +46,16 @@ def _delete_folder_dialog(folder_rel: str, folder_name: str):
@st.dialog("Add File")
def _add_file_dialog(parent_path: str = ""):
name = st.text_input("File name:", placeholder="e.g. script.py")
col1, col2 = st.columns(2)
with col1:
if st.button("Create", type="primary", use_container_width=True):
with st.form("add_file_form"):
name = st.text_input("File name:", placeholder="e.g. script.py")
col1, col2 = st.columns(2)
with col1:
submitted = st.form_submit_button("Create", type="primary", use_container_width=True)
with col2:
cancel = st.form_submit_button("Cancel", use_container_width=True)
if submitted:
if not name.strip():
st.warning("Please enter a file name.")
elif "/" in name or "\\" in name:
@ -57,17 +63,23 @@ def _add_file_dialog(parent_path: str = ""):
else:
if fm.create_file(parent_path, name.strip()):
st.rerun()
with col2:
if st.button("Cancel", use_container_width=True):
if cancel:
st.rerun()
@st.dialog("Add Folder")
def _add_folder_dialog(parent_path: str = ""):
name = st.text_input("Folder name:", placeholder="e.g. utils")
col1, col2 = st.columns(2)
with col1:
if st.button("Create", type="primary", use_container_width=True):
with st.form("add_folder_form"):
name = st.text_input("Folder name:", placeholder="e.g. utils")
col1, col2 = st.columns(2)
with col1:
submitted = st.form_submit_button("Create", type="primary", use_container_width=True)
with col2:
cancel = st.form_submit_button("Cancel", use_container_width=True)
if submitted:
if not name.strip():
st.warning("Please enter a folder name.")
elif "/" in name or "\\" in name:
@ -75,6 +87,98 @@ def _add_folder_dialog(parent_path: str = ""):
else:
if fm.create_folder(parent_path, name.strip()):
st.rerun()
if cancel:
st.rerun()
@st.dialog("Rename File")
def _rename_file_dialog(relative_file_path: str, file_name: str):
"""
Dialog to rename a file.
Args:
relative_file_path (str): The current relative path (without base path) to the file to rename, including the file name.
file_name (str): The current name of the file, including the extension.
"""
st.write(f"Current name: **{file_name}**")
with st.form("rename_file_form"):
new_name = st.text_input(
"New name:",
value=Path(relative_file_path).stem
)
col1, col2 = st.columns(2)
with col1:
submitted = st.form_submit_button(
"Confirm",
type="primary",
use_container_width=True
)
with col2:
cancel = st.form_submit_button(
"Cancel",
use_container_width=True
)
if submitted:
if not new_name.strip():
st.warning("Please enter a name.")
elif "/" in new_name or "\\" in new_name or "." in new_name:
st.warning("Name must not contain slashes.")
else:
if fm.rename_file(relative_file_path, new_name.strip()):
absolute_file_path = str(Path(fm.base_path / relative_file_path))
ext = Path(absolute_file_path).suffix
new_file_path = str(
Path(absolute_file_path).parent / (Path(new_name.strip()).stem + ext)
)
if absolute_file_path in st.session_state.open_files:
i = st.session_state.open_files.index(absolute_file_path)
st.session_state.open_files[i] = new_file_path
if absolute_file_path in st.session_state.files_content:
st.session_state.files_content[new_file_path] = \
st.session_state.files_content.pop(absolute_file_path)
if st.session_state.active_file == absolute_file_path:
st.session_state.active_file = new_file_path
st.rerun()
else:
st.error("Rename failed. Check that the file still exists.")
st.error(f"Attempted to rename: {relative_file_path} to {new_name.strip()}")
if cancel:
st.rerun()
@st.dialog("Delete File")
def _delete_file_dialog(relative_file_path: str, file_name: str):
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_file_path):
abs_file_path = str(Path(fm.base_path) / relative_file_path)
print(f"Deleting file at absolute path: {abs_file_path}") # Debugging info
print(f"Current open files before deletion: {st.session_state.open_files}") # Debugging info
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()
else:
st.error("Delete failed. Check that the file still exists.")
with col2:
if st.button("Cancel", use_container_width=True):
st.rerun()
@ -186,6 +290,17 @@ def render_sidebar():
_add_folder_dialog(folder_rel)
if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True):
_delete_folder_dialog(folder_rel, folder_name)
if st.session_state.get("active_file"):
active_file_name = Path(st.session_state.active_file).name
file_rel = str(Path(st.session_state.active_file).relative_to(fm.base_path))
with st.container(border=True):
st.write(f"**File actions:** {active_file_name}")
if st.button("Rename File", key="btn_rename_file", use_container_width=True):
_rename_file_dialog(file_rel, active_file_name)
if st.button("Delete File", key="btn_delete_active_file", use_container_width=True):
_delete_file_dialog(file_rel, active_file_name)
with add_more:
with st.popover("⚙️ Explorer Options", key="popover_options", use_container_width=True):

View File

@ -19,12 +19,20 @@ def init_state():
# Editor state initialization
if "open_files" not in st.session_state:
"""A list of currently open file paths - absolute paths only. The order determines the tab order in the UI.
Format: [ "path/to/file1.py", "path/to/file2.js", ... ]
"""
st.session_state.open_files = []
if "files_content" not in st.session_state:
"""A dictionary mapping file paths to their current content in the editor.
Format: { "path/to/file.py": "file content as string", ... }
"""
st.session_state.files_content = {}
if "active_file" not in st.session_state:
"""The currently active file in the editor (absolute path in string e.g. "/workspace/path/to/file.py").
Should be one of the paths in open_files or None if no file is open."""
st.session_state.active_file = None
if "active_tab" not in st.session_state: