rename and delet options per file in File Tree/sidebar
This commit is contained in:
parent
c00a2454fc
commit
73ba4a7931
@ -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 = {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user