diff --git a/CMakeLists.txt b/CMakeLists.txt index cb09344..389bf77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,141 +1,35 @@ cmake_minimum_required(VERSION 3.16) -# Project name -set(TARGET_NAME CodeAstraApp) -set(EXECUTABLE_NAME CodeAstra) +project(CodeAstra VERSION 0.1.0 DESCRIPTION "Code Editor written in modern C++ using Qt6") -set(QT_MAJOR_VERSION 6) - -project(${TARGET_NAME} VERSION 0.0.1 DESCRIPTION "Code Editor written in C++ using Qt6") - -# Enable automatic MOC (Meta-Object Compiler) handling for Qt -set(CMAKE_AUTOMOC ON) - -# Set the CXX standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Set default build output directories -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) -set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}) +# Use cmake/ for custom modules +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") -# Detect operating system -if(WIN32) - set(OS_NAME "Windows") -elseif(APPLE) - set(OS_NAME "macOS") -else() - set(OS_NAME "Linux") -endif() +# Set output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -message(STATUS "Building for ${OS_NAME}") +# Enable Qt tools +set(CMAKE_AUTOMOC ON) -# Locate Qt installation -if(DEFINED ENV{Qt${QT_MAJOR_VERSION}_HOME}) - set(Qt_DIR "$ENV{Qt${QT_MAJOR_VERSION}_HOME}") - message(STATUS "Using Qt from: ${Qt_DIR}") -else() - if(WIN32) - set(Qt_DIR "C:/Qt/${QT_MAJOR_VERSION}/msvc2022_64/lib/cmake/Qt${QT_MAJOR_VERSION}") - elseif(APPLE) - set(Qt_DIR "/usr/local/opt/qt/lib/cmake/Qt${QT_MAJOR_VERSION}") - else() - set(Qt_DIR "/usr/lib/cmake/Qt${QT_MAJOR_VERSION}") - endif() - message(STATUS "Using default Qt path: ${Qt_DIR}") -endif() +# Define target names +set(TARGET_NAME CodeAstra) +set(EXECUTABLE_NAME ${TARGET_NAME}App) -# Set Qt path for find_package -set(CMAKE_PREFIX_PATH ${Qt_DIR}) +# Set Qt version +set(QT_MAJOR_VERSION 6) -# Find Qt components +# Find Qt find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS Core Widgets Test) -# Locate yaml-cpp -if(APPLE) - set(yaml-cpp_DIR "/opt/homebrew/Cellar/yaml-cpp/0.8.0/lib/cmake/yaml-cpp") -endif() +# yaml-cpp find_package(yaml-cpp REQUIRED CONFIG) -# Copy YAML files to the build directory (non-macOS case) -set(YAML_SOURCE_DIR ${CMAKE_SOURCE_DIR}/config) -set(YAML_DEST_DIR ${CMAKE_BINARY_DIR}/config) -file(MAKE_DIRECTORY ${YAML_DEST_DIR}) -file(GLOB YAML_FILES "${YAML_SOURCE_DIR}/*.yaml") - -foreach(YAML_FILE ${YAML_FILES}) - file(COPY ${YAML_FILE} DESTINATION ${YAML_DEST_DIR}) -endforeach() - -# Create the CodeAstra library -add_library(${TARGET_NAME} - src/MainWindow.cpp - src/CodeEditor.cpp - src/Tree.cpp - src/FileManager.cpp - src/Syntax.cpp - src/SyntaxManager.cpp - include/MainWindow.h - include/CodeEditor.h - include/Tree.h - include/LineNumberArea.h - include/FileManager.h - include/SyntaxManager.h - include/Syntax.h -) - -# Link YAML-CPP to the CodeAstra library -target_link_libraries(${TARGET_NAME} PRIVATE yaml-cpp) - -# Create the executable for the application -add_executable(${EXECUTABLE_NAME} src/main.cpp) - -# Ensure YAML config files are copied into macOS app bundle -# if(APPLE) -# add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD -# COMMAND ${CMAKE_COMMAND} -E make_directory "$/Contents/Resources/config" -# COMMAND ${CMAKE_COMMAND} -E copy_directory "${YAML_SOURCE_DIR}" "$/Contents/Resources/config" -# COMMENT "Copying YAML config files into macOS app bundle..." -# ) -# endif() - -# Link the main executable with the CodeAstra library and Qt libraries -target_link_libraries(${EXECUTABLE_NAME} PRIVATE ${TARGET_NAME} Qt${QT_MAJOR_VERSION}::Core Qt${QT_MAJOR_VERSION}::Widgets) - -# Add the tests subdirectory +# Add subdirectories +add_subdirectory(src) add_subdirectory(tests) -# Qt resource files -qt_add_resources(APP_RESOURCES resources.qrc) -target_sources(${EXECUTABLE_NAME} PRIVATE ${APP_RESOURCES}) - -# Compiler flags per OS -if(MSVC) - target_compile_options(${EXECUTABLE_NAME} PRIVATE /W4 /WX) -elseif(APPLE) - target_compile_options(${EXECUTABLE_NAME} PRIVATE -Wall -Wextra -pedantic -Werror) - # set_target_properties(${EXECUTABLE_NAME} PROPERTIES MACOSX_BUNDLE TRUE) -else() - target_compile_options(${EXECUTABLE_NAME} PRIVATE -Wall -Wextra -pedantic -Werror) -endif() - -# Include directories -target_include_directories(${EXECUTABLE_NAME} PRIVATE ${Qt${QT_MAJOR_VERSION}_INCLUDE_DIRS}) -target_include_directories(${EXECUTABLE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) -target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) - -# Set output names properly for Debug and Release -set_target_properties(${EXECUTABLE_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}" - DEBUG_OUTPUT_NAME "${EXECUTABLE_NAME}d" - RELEASE_OUTPUT_NAME ${EXECUTABLE_NAME} -) - -# Link necessary Qt libraries to CodeAstra library -target_link_libraries(${TARGET_NAME} PRIVATE Qt${QT_MAJOR_VERSION}::Core Qt${QT_MAJOR_VERSION}::Widgets) - -# Ensure correct linking of yaml-cpp (macOS) -if(APPLE) - target_include_directories(${EXECUTABLE_NAME} PRIVATE /opt/homebrew/include) - target_link_directories(${EXECUTABLE_NAME} PRIVATE /opt/homebrew/lib) -endif() \ No newline at end of file diff --git a/Makefile b/Makefile index 2a564b8..329778d 100644 --- a/Makefile +++ b/Makefile @@ -1,42 +1,39 @@ PROJECT = CodeAstra BUILD_DIR = $(PWD)/build -EXECUTABLE = $(PROJECT) -# Set CMake options CMAKE_OPTIONS = .. -# Default target: Run CMake and install the project -all: build install +.PHONY: all build clean install build_tests test + +all: install -# Run CMake to build the project build: - @echo "Building $(PROJECT) with CMake..." + @echo "Building $(PROJECT)..." @mkdir -p $(BUILD_DIR) @cd $(BUILD_DIR) && cmake $(CMAKE_OPTIONS) -# Clean the build directory clean: @echo "Cleaning the build directory..." @rm -rf $(BUILD_DIR) -# Uninstalling the software -uninstall: clean - @echo "Uninstalling $(PROJECT)..." - @rm -rf $(EXECUTABLE).app $(EXECUTABLE)d.app - -# Install the project install: build @echo "Installing $(PROJECT)..." - @cd $(BUILD_DIR) && make - @echo "$(PROJECT) installed." + @cmake --build $(BUILD_DIR) + @echo "Installation complete." build_tests: build - @cd $(BUILD_DIR)/tests/ && make + @echo "Building tests..." + @$(MAKE) -C $(BUILD_DIR)/tests test: build_tests + @echo "Running tests..." @for test in ./build/tests/test_*; do \ if [ -f $$test ]; then \ echo "Running $$test..."; \ $$test; \ fi; \ done + +run: + @echo "Running $(PROJECT)..." + @./build/bin/$(PROJECT) \ No newline at end of file diff --git a/README.md b/README.md index b60d19a..22ba0b4 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Please, check the [wiki](https://github.com/sandbox-science/CodeAstra/wiki) for - [ ] Create a new file ~ in progress - [x] File tree navigation - [ ] Syntax highlighting ~ in progress - - Supported Languagues: + - Supported Languages: - [x] Markdown (**foundation**) - [x] YAML (**foundation**) - [ ] C/C++ (**in progress**) @@ -63,4 +63,4 @@ Please, check the [wiki](https://github.com/sandbox-science/CodeAstra/wiki) for - [ ] Plugin system ## To-Do -Find tasks to-do on our open [issues](https://github.com/sandbox-science/CodeAstra/issues) +Find tasks to do on our open [issues](https://github.com/sandbox-science/CodeAstra/issues) diff --git a/include/CodeEditor.h b/include/CodeEditor.h index dd8afa8..5ec9ab6 100644 --- a/include/CodeEditor.h +++ b/include/CodeEditor.h @@ -28,6 +28,7 @@ class CodeEditor : public QPlainTextEdit Mode mode = NORMAL; void lineNumberAreaPaintEvent(QPaintEvent *event); int lineNumberAreaWidth(); + void autoIndentation(); signals: void statusMessageChanged(const QString &message); diff --git a/include/FileManager.h b/include/FileManager.h index 178e8f3..6a44b39 100644 --- a/include/FileManager.h +++ b/include/FileManager.h @@ -3,10 +3,17 @@ #include #include #include +#include class CodeEditor; class MainWindow; +struct OperationResult +{ + bool success; + std::string message; +}; + /** * @class FileManager * @brief Manages file operations such as creating, saving, and opening files. @@ -25,9 +32,11 @@ class FileManager : public QObject static FileManager &getInstance(CodeEditor *editor = nullptr, MainWindow *mainWindow = nullptr) { static FileManager instance(editor, mainWindow); + if (editor && mainWindow) { + instance.initialize(editor, mainWindow); + } return instance; } - FileManager(const FileManager &) = delete; FileManager &operator=(const FileManager &) = delete; @@ -37,6 +46,12 @@ class FileManager : public QObject void setCurrentFileName(const QString fileName); void initialize(CodeEditor *editor, MainWindow *mainWindow); + static OperationResult renamePath(const QFileInfo &pathInfo, const QString &newName); + static OperationResult newFile(const QFileInfo &pathInfo, QString newFilePath); + static OperationResult newFolder(const QFileInfo &pathInfo, QString newFolderPath); + static OperationResult duplicatePath(const QFileInfo &pathInfo); + static OperationResult deletePath(const QFileInfo &pathInfo); + public slots: void newFile(); void saveFile(); @@ -54,4 +69,4 @@ public slots: MainWindow *m_mainWindow; QSyntaxHighlighter *m_currentHighlighter = nullptr; QString m_currentFileName; -}; +}; \ No newline at end of file diff --git a/include/Tree.h b/include/Tree.h index 210c733..856e71f 100644 --- a/include/Tree.h +++ b/include/Tree.h @@ -1,14 +1,16 @@ #pragma once +#include "FileManager.h" + #include #include #include +#include // Forward declarations class QTreeView; class QFileSystemModel; class QFileIconProvider; -class FileManager; /** * @class Tree @@ -30,8 +32,12 @@ class Tree : public QObject void setupTree(); void openFile(const QModelIndex &index); + QFileSystemModel* getModel() const; + private: void showContextMenu(const QPoint &pos); + QFileInfo getPathInfo(); + void isSuccessful(OperationResult result); std::unique_ptr m_iconProvider; std::unique_ptr m_model; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..04d9147 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,58 @@ +set(TARGET_NAME CodeAstraApp) +set(EXECUTABLE_NAME CodeAstra) + +# Source files +set(SOURCES + MainWindow.cpp + CodeEditor.cpp + Tree.cpp + FileManager.cpp + Syntax.cpp + SyntaxManager.cpp +) + +# Headers +set(HEADERS + ${CMAKE_SOURCE_DIR}/include/MainWindow.h + ${CMAKE_SOURCE_DIR}/include/CodeEditor.h + ${CMAKE_SOURCE_DIR}/include/Tree.h + ${CMAKE_SOURCE_DIR}/include/FileManager.h + ${CMAKE_SOURCE_DIR}/include/Syntax.h + ${CMAKE_SOURCE_DIR}/include/SyntaxManager.h + ${CMAKE_SOURCE_DIR}/include/LineNumberArea.h +) + +# Find yaml-cpp using CMake's package config +find_package(yaml-cpp REQUIRED) + +# Library +add_library(${TARGET_NAME} ${SOURCES} ${HEADERS}) +target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) + +# Link against the proper target +target_link_libraries(${TARGET_NAME} PRIVATE Qt${QT_MAJOR_VERSION}::Core Qt${QT_MAJOR_VERSION}::Widgets yaml-cpp::yaml-cpp) +set_target_properties(${TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# Executable +add_executable(${EXECUTABLE_NAME} ${CMAKE_SOURCE_DIR}/src/main.cpp) +target_link_libraries(${EXECUTABLE_NAME} PRIVATE ${TARGET_NAME} Qt6::Core Qt6::Widgets) +target_include_directories(${EXECUTABLE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) + +# Resources +qt_add_resources(APP_RESOURCES ${CMAKE_SOURCE_DIR}/resources.qrc) +target_sources(${EXECUTABLE_NAME} PRIVATE ${APP_RESOURCES}) + +# OS-specific flags +if(MSVC) + target_compile_options(${EXECUTABLE_NAME} PRIVATE /W4 /WX /analyze /sdl /guard:cf) +elseif(APPLE OR UNIX) + target_compile_options(${EXECUTABLE_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -Wsign-conversion -fsanitize=address,undefined -fstack-protector) + target_link_options(${EXECUTABLE_NAME} PRIVATE -fsanitize=address,undefined) +endif() + +# Copy config files +file(GLOB YAML_FILES "${CMAKE_SOURCE_DIR}/config/*.yaml") +file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/config) +foreach(YAML_FILE ${YAML_FILES}) + configure_file(${YAML_FILE} ${CMAKE_BINARY_DIR}/config/ COPYONLY) +endforeach() diff --git a/src/CodeEditor.cpp b/src/CodeEditor.cpp index 0d23421..63815f9 100644 --- a/src/CodeEditor.cpp +++ b/src/CodeEditor.cpp @@ -30,12 +30,7 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) moveCursor(QTextCursor::WordLeft, QTextCursor::KeepAnchor); return; } - if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Slash) - { - addComment(); - return; - } - + if (mode == NORMAL) { switch (event->key()) @@ -61,6 +56,7 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) break; } } + else if (mode == INSERT) { if (event->key() == Qt::Key_Escape) @@ -68,6 +64,23 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) mode = NORMAL; emit statusMessageChanged("Normal mode activated. Press 'escape' to return to normal mode."); } + else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + { + autoIndentation(); + return; + } + else if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Slash) + { + addComment(); + return; + } + else if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Backspace) + { + moveCursor(QTextCursor::WordLeft, QTextCursor::KeepAnchor); + textCursor().removeSelectedText(); + textCursor().deletePreviousChar(); + return; + } else { QPlainTextEdit::keyPressEvent(event); @@ -75,6 +88,36 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) } } +// Add auto indentation when writing code and pressing enter keyboard key +void CodeEditor::autoIndentation() +{ + auto cursor = textCursor(); + auto currentBlock = cursor.block(); + QString currentText = currentBlock.text(); + + int indentLevel = 0; + for (int i = 0; i < currentText.size(); ++i) + { + if (currentText.at(i) == ' ') + { + ++indentLevel; + } + + else if (currentText.at(i) == '\t') + { + indentLevel += 4; + } + + else + { + break; + } + } + + cursor.insertText("\n" + QString(indentLevel, ' ')); + setTextCursor(cursor); +} + void CodeEditor::addLanguageSymbol(QTextCursor &cursor, const QString &commentSymbol) { if (cursor.hasSelection()) diff --git a/src/FileManager.cpp b/src/FileManager.cpp index f05f3fa..7d35e1b 100644 --- a/src/FileManager.cpp +++ b/src/FileManager.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include FileManager::FileManager(CodeEditor *editor, MainWindow *mainWindow) : m_editor(editor), m_mainWindow(mainWindow) @@ -157,3 +160,193 @@ QString FileManager::getDirectoryPath() const nullptr, QObject::tr("Open Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); } + +// Check path to prevent path traversal attack +bool isValidPath(const std::filesystem::path &path) +{ + std::string pathStr = path.string(); + if (pathStr.find("..") != std::string::npos) + { + return false; + } + + return true; +} + +OperationResult FileManager::renamePath(const QFileInfo &pathInfo, const QString &newName) +{ + if (!pathInfo.exists()) + { + return {false, "Path does not exist: " + pathInfo.fileName().toStdString()}; + } + + std::filesystem::path oldPath = pathInfo.absoluteFilePath().toStdString(); + + // Validate the input path + if (!isValidPath(oldPath)) + { + return {false, "Invalid file path."}; + } + + std::filesystem::path newPath = oldPath.parent_path() / newName.toStdString(); + + if (QFileInfo(newPath).exists()) + { + return {false, newPath.filename().string() + " already takken."}; + } + + try + { + std::filesystem::rename(oldPath, newPath); + } + catch (const std::filesystem::filesystem_error &e) + { + QMessageBox::critical(nullptr, "Error", QString(e.what())); + return {false, e.what()}; + } + + return {true, newPath.filename().string()}; +} + +// Check if the path is a valid directory +// and not a system or home directory +bool isAValidDirectory(const QFileInfo &pathInfo) +{ + if (!pathInfo.exists()) + { + qWarning() << "Path does not exist: " << pathInfo.fileName(); + return false; + } + + if (pathInfo.absolutePath() == "/" || pathInfo.absolutePath() == QDir::homePath()) + { + QMessageBox::critical(nullptr, "Error", "Cannot delete system or home directory."); + return false; + } + + return true; +} + +OperationResult FileManager::deletePath(const QFileInfo &pathInfo) +{ + if (!isAValidDirectory(pathInfo)) + { + return {false, "ERROR: invalid folder path." + pathInfo.absolutePath().toStdString()}; + } + + std::filesystem::path pathToDelete = pathInfo.absoluteFilePath().toStdString(); + + // Validate the input path + if (!isValidPath(pathToDelete)) + { + return {false, "ERROR: invalid file path." + pathToDelete.filename().string()}; + } + + if (!QFile::moveToTrash(pathToDelete)) + { + return {false, "ERROR: failed to delete: " + pathToDelete.string()}; + } + + return {true, pathToDelete.filename().string()}; +} + +OperationResult FileManager::newFile(const QFileInfo &pathInfo, QString newFilePath) +{ + std::filesystem::path dirPath = pathInfo.absolutePath().toStdString(); + + if (pathInfo.isDir()) + { + dirPath = pathInfo.absoluteFilePath().toStdString(); + } + + if (!isValidPath(dirPath)) + { + return {false, "invalid file path."}; + } + + std::filesystem::path filePath = dirPath / newFilePath.toStdString(); + if (QFileInfo(filePath).exists()) + { + return {false, filePath.filename().string() + " already used."}; + } + + std::ofstream file(filePath); + if (file.is_open()) + { + file.close(); + } + qDebug() << "New file created."; + + FileManager::getInstance().setCurrentFileName(QString::fromStdString(filePath.string())); + return {true, filePath.filename().string()}; +} + +OperationResult FileManager::newFolder(const QFileInfo &pathInfo, QString newFolderPath) +{ + // TO-DO: look up which is prefered: error_code or exception + std::error_code err{}; + std::filesystem::path dirPath = pathInfo.absolutePath().toStdString(); + + // Check if the path is a directory + if (pathInfo.isDir()) + { + dirPath = pathInfo.absoluteFilePath().toStdString(); + } + + // Validate the input path + if (!isValidPath(dirPath)) + { + return {false, "Invalid file path."}; + } + + std::filesystem::path newPath = dirPath / newFolderPath.toStdString(); + if (QFileInfo(newPath).exists()) + { + return {false, newPath.filename().string() + " already used."}; + } + + std::filesystem::create_directory(newPath, err); + if (err) + { + qDebug() << "Error creating directory:" << QString::fromStdString(err.message()); + return {false, err.message().c_str()}; + } + + qDebug() << "New folder created at:" << QString::fromStdString(newPath.string()); + + return {true, newPath.filename().string()}; +} + +OperationResult FileManager::duplicatePath(const QFileInfo &pathInfo) +{ + std::filesystem::path filePath = pathInfo.absoluteFilePath().toStdString(); + + // Validate the input path + if (!isValidPath(filePath)) + { + return {false , "Invalid path."}; + } + + std::string fileName = filePath.stem().string(); + std::filesystem::path dupPath = filePath.parent_path() / (fileName + "_copy" + filePath.extension().c_str()); + + int counter = 1; + while (QFileInfo(dupPath).exists()) + { + dupPath = filePath.parent_path() / (fileName + "_copy" + std::to_string(counter) + filePath.extension().c_str()); + counter++; + } + + try + { + std::filesystem::copy(filePath, dupPath, std::filesystem::copy_options::recursive); // copy_option is needed for duplicating nested directories + } + catch (const std::filesystem::filesystem_error &e) + { + return {false, e.what()}; + } + + qDebug() << "Duplicated file to:" << QString::fromStdString(dupPath.string()); + + return {true, dupPath.filename().string()}; +} \ No newline at end of file diff --git a/src/Tree.cpp b/src/Tree.cpp index 185d0ab..581685a 100644 --- a/src/Tree.cpp +++ b/src/Tree.cpp @@ -1,6 +1,5 @@ #include "Tree.h" #include "CodeEditor.h" -#include "FileManager.h" #include #include @@ -8,6 +7,9 @@ #include #include #include +#include +#include +#include Tree::Tree(QSplitter *splitter) : QObject(splitter), @@ -69,6 +71,13 @@ void Tree::openFile(const QModelIndex &index) FileManager::getInstance().loadFileInEditor(filePath); } +QFileSystemModel *Tree::getModel() const +{ + if (!m_model) + throw std::runtime_error("Tree model is not initialized!"); + return m_model.get(); +} + // Context menu for file operations // such as creating new files, folders, renaming, and deleting // This function is called when the user right-clicks on the tree view @@ -78,25 +87,143 @@ void Tree::showContextMenu(const QPoint &pos) QAction *newFileAction = contextMenu.addAction("New File"); QAction *newFolderAction = contextMenu.addAction("New Folder"); + contextMenu.addSeparator(); QAction *renameAction = contextMenu.addAction("Rename"); + QAction *duplicateAction = contextMenu.addAction("Duplicate"); + contextMenu.addSeparator(); QAction *deleteAction = contextMenu.addAction("Delete"); QAction *selectedAction = contextMenu.exec(m_tree->viewport()->mapToGlobal(pos)); if (selectedAction == newFileAction) { - // TO-DO: implement file creation + QFileInfo pathInfo = getPathInfo(); + if (!pathInfo.exists()) + { + qWarning() << "Path does not exist: " << pathInfo.fileName(); + return; + } + + bool ok; + QString newFileName = QInputDialog::getText( + nullptr, + "New File", + "Enter file name:", + QLineEdit::Normal, + nullptr, + &ok + ); + + if (ok && !newFileName.isEmpty()) + { + OperationResult result = FileManager::getInstance().newFile(pathInfo, newFileName); + isSuccessful(result); + } } else if (selectedAction == newFolderAction) { - // TO-DO: implement folder creation + QFileInfo pathInfo = getPathInfo(); + if (!pathInfo.exists()) + { + qWarning() << "Path does not exist: " << pathInfo.fileName(); + return; + } + + bool ok; + QString newFolderName = QInputDialog::getText( + nullptr, + "New Folder", + "Enter folder name:", + QLineEdit::Normal, + nullptr, + &ok + ); + + if (ok && !newFolderName.isEmpty()) + { + OperationResult result = FileManager::getInstance().newFolder(pathInfo, newFolderName); + isSuccessful(result); + } + } + else if (selectedAction == duplicateAction) + { + QFileInfo pathInfo = getPathInfo(); + if (!pathInfo.exists()) + { + qWarning() << "File does not exist: " << pathInfo.fileName(); + return; + } + + OperationResult result = FileManager::getInstance().duplicatePath(pathInfo); + isSuccessful(result); } else if (selectedAction == renameAction) { - // TO-DO: implement rename file/folder + QFileInfo oldPathInfo = getPathInfo(); + if (!oldPathInfo.exists()) + { + qWarning() << "File does not exist: " << oldPathInfo.fileName(); + return; + } + + bool ok; + QString newFileName = QInputDialog::getText( + nullptr, + "Rename File", + "Enter new file name:", + QLineEdit::Normal, + oldPathInfo.fileName(), + &ok); + + if (ok && !newFileName.isEmpty()) + { + OperationResult result = FileManager::getInstance().renamePath(oldPathInfo, newFileName); + isSuccessful(result); + } } else if (selectedAction == deleteAction) { - // TO-DO: implement file deletion + QFileInfo pathInfo = getPathInfo(); + if (!pathInfo.exists()) + { + qWarning() << "File does not exist: " << pathInfo.fileName(); + return; + } + QMessageBox::StandardButton reply = QMessageBox::question(nullptr, "Confirm Deletion", + "Are you sure you want to delete\n'" + pathInfo.fileName() + "'?", + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::No) + { + qInfo() << "Deletion cancelled."; + } + else + { + OperationResult result = FileManager::getInstance().deletePath(pathInfo); + isSuccessful(result); + } } } + +QFileInfo Tree::getPathInfo() +{ + QModelIndex index = m_tree->currentIndex(); + if (!index.isValid()) + { + qWarning() << "Invalid index."; + return QFileInfo(); + } + + return QFileInfo(m_model->filePath(index)); +} + +void Tree::isSuccessful(OperationResult result) +{ + if (result.success) + { + qInfo() << QString::fromStdString(result.message) << " created successfully."; + } + else + { + QMessageBox::critical(nullptr, "Error", QString::fromStdString(result.message)); + } +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b1cd9dd..1f00856 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,47 +1,27 @@ -# Enable testing enable_testing() -find_package(yaml-cpp REQUIRED) -# Add individual test executables -add_executable(test_mainwindow test_mainwindow.cpp) -add_executable(test_tree test_tree.cpp) -add_executable(test_syntax test_syntax.cpp) - -# Make sure the test executables link with the necessary libraries -target_link_libraries(test_mainwindow PRIVATE ${TARGET_NAME} Qt6::Widgets Qt6::Test yaml-cpp) -target_link_libraries(test_tree PRIVATE ${TARGET_NAME} Qt6::Widgets Qt6::Test yaml-cpp) -target_link_libraries(test_syntax PRIVATE ${TARGET_NAME} Qt6::Widgets Qt6::Test yaml-cpp) - -# Register each test with CTest -add_test(NAME test_mainwindow COMMAND test_mainwindow) -add_test(NAME test_tree COMMAND test_tree) -add_test(NAME test_syntax COMMAND test_tree) - -# Set the runtime output directory for the test executables -set_target_properties(test_mainwindow PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/tests -) -set_target_properties(test_tree PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/tests -) -set_target_properties(test_syntax PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/tests -) -set_property(SOURCE test_mainwindow.cpp PROPERTY SKIP_AUTOMOC OFF) -set_property(SOURCE test_tree.cpp PROPERTY SKIP_AUTOMOC OFF) -set_property(SOURCE test_syntax.cpp PROPERTY SKIP_AUTOMOC OFF) +find_package(yaml-cpp REQUIRED CONFIG) -# Include directories for tests -target_include_directories(test_mainwindow PRIVATE ${CMAKE_SOURCE_DIR}/include) -target_include_directories(test_tree PRIVATE ${CMAKE_SOURCE_DIR}/include) -target_include_directories(test_syntax PRIVATE ${CMAKE_SOURCE_DIR}/include) - -# Ensure proper linking directories are set for yaml-cpp -target_include_directories(test_mainwindow PRIVATE /opt/homebrew/include) -target_include_directories(test_tree PRIVATE /opt/homebrew/include) -target_include_directories(test_syntax PRIVATE /opt/homebrew/include) - -target_link_directories(test_mainwindow PRIVATE /opt/homebrew/lib) -target_link_directories(test_tree PRIVATE /opt/homebrew/lib) -target_link_directories(test_syntax PRIVATE /opt/homebrew/lib) +# Add test executables +add_executable(test_mainwindow test_mainwindow.cpp) +add_executable(test_filemanager test_filemanager.cpp) +add_executable(test_syntax test_syntax.cpp) +# Link libraries +foreach(test_target IN ITEMS test_mainwindow test_filemanager test_syntax) + target_link_libraries(${test_target} PRIVATE + ${EXECUTABLE_NAME} + Qt6::Widgets + Qt6::Test + yaml-cpp::yaml-cpp + ) + target_include_directories(${test_target} PRIVATE + ${CMAKE_SOURCE_DIR}/include + ) + set_target_properties(${test_target} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/tests + ) + set_property(SOURCE ${test_target}.cpp PROPERTY SKIP_AUTOMOC OFF) + + add_test(NAME ${test_target} COMMAND ${test_target}) +endforeach() diff --git a/tests/test_filemanager.cpp b/tests/test_filemanager.cpp new file mode 100644 index 0000000..9e87e9f --- /dev/null +++ b/tests/test_filemanager.cpp @@ -0,0 +1,169 @@ +#include "Tree.h" +#include "FileManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class TestFileManager : public QObject +{ + Q_OBJECT + +private: + QSplitter *splitter = nullptr; + Tree *tree = nullptr; + +private slots: + void initTestCase(); + void cleanupTestCase(); + void testOpenFile_invalid(); + void testDeleteFile(); + void testDeleteDir(); + void testRenamePath(); + void testNewFile(); + void testNewFolder(); + void testNewFolderFail(); + void testDuplicatePath(); +}; + +void TestFileManager::initTestCase() +{ + qDebug() << "Initializing TestFileManager tests..."; + splitter = new QSplitter; + tree = new Tree(splitter); +} + +void TestFileManager::cleanupTestCase() +{ + qDebug() << "Cleaning up TestFileManager tests..."; + delete tree; + delete splitter; +} + +void TestFileManager::testOpenFile_invalid() +{ + QModelIndex index; + tree->openFile(index); + + QVERIFY2(FileManager::getInstance().getCurrentFileName().isEmpty(), + "FileManager should not process an invalid file."); +} + +void TestFileManager::testRenamePath() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString originalFilePath = tempDir.path() + "/testFile.cpp"; + QFile file(originalFilePath); + QVERIFY2(file.open(QIODevice::WriteOnly), "File should be created successfully."); + file.write("// test content"); + file.close(); + + QString newFilePath = tempDir.path() + "/renamedTestFile.cpp"; + OperationResult fileRenamed = FileManager::getInstance().renamePath(QFileInfo(originalFilePath), newFilePath); + + QVERIFY2(fileRenamed.success, fileRenamed.message.c_str()); + QVERIFY2(QFile::exists(newFilePath), "Renamed file should exist."); + QVERIFY2(!QFile::exists(originalFilePath), "Original file should no longer exist."); +} + +void TestFileManager::testDeleteFile() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString tempFilePath = tempDir.path() + "/testDeleteFile.cpp"; + QFile file(tempFilePath); + QVERIFY2(file.open(QIODevice::WriteOnly), "Temporary file should be created."); + file.close(); + + QVERIFY2(QFile::exists(tempFilePath), "Temporary file should exist before deletion."); + + QFileSystemModel *model = tree->getModel(); + QVERIFY2(model, "Tree model should not be null."); + + QModelIndex index = model->index(tempFilePath); + QVERIFY2(index.isValid(), "Model index should be valid for the temporary file."); + + FileManager::getInstance().deletePath(QFileInfo(model->filePath(index))); + + QVERIFY2(!QFile::exists(tempFilePath), "Temporary file should be deleted."); +} + +void TestFileManager::testDeleteDir() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString dirPath = tempDir.path() + "/testDeleteDir"; + QDir().mkdir(dirPath); + + QVERIFY2(QFileInfo(dirPath).exists(), "Test directory should exist before deletion."); + + QFileSystemModel *model = tree->getModel(); + QVERIFY2(model, "Tree model should not be null."); + + QModelIndex index = model->index(dirPath); + QVERIFY2(index.isValid(), "Model index should be valid for the test directory."); + + FileManager::getInstance().deletePath(QFileInfo(model->filePath(index))); + + QVERIFY2(!QFile::exists(dirPath), "Directory should be deleted."); +} + +void TestFileManager::testNewFile() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString folderPath = tempDir.path(); + OperationResult fileCreated = FileManager::getInstance().newFile(QFileInfo(folderPath), "newFileTest1.c"); + + QVERIFY2(fileCreated.success, "New file should be created."); + QVERIFY2(QFile::exists(folderPath + "/newFileTest1.c"), "Newly created file should exist."); +} + +void TestFileManager::testNewFolder() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString folderPath = tempDir.path(); + OperationResult folderCreated = FileManager::getInstance().newFolder(QFileInfo(folderPath), "newDirTest"); + + QVERIFY2(folderCreated.success, "New folder should be created."); + QVERIFY2(QFile::exists(folderPath + "/newDirTest"), "Newly created folder should exist."); +} + +void TestFileManager::testNewFolderFail() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString folderPath = tempDir.path(); + OperationResult folderCreated = FileManager::getInstance().newFolder(QFileInfo(folderPath), ""); + + QVERIFY2(!folderCreated.success, "Folder creation should fail."); +} + +void TestFileManager::testDuplicatePath() +{ + QTemporaryDir tempDir; + QVERIFY2(tempDir.isValid(), "Temporary directory should be valid."); + + QString basePath = tempDir.path() + "/testDuplicateDir"; + QDir().mkdir(basePath); + + OperationResult pathDuplicated = FileManager::getInstance().duplicatePath(QFileInfo(basePath)); + + QVERIFY2(pathDuplicated.success, "Path should be duplicated successfully."); +} + +QTEST_MAIN(TestFileManager) +#include "test_filemanager.moc" diff --git a/tests/test_tree.cpp b/tests/test_tree.cpp deleted file mode 100644 index f00465c..0000000 --- a/tests/test_tree.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include -#include -#include "Tree.h" -#include "FileManager.h" -#include "CodeEditor.h" - -#include -#include -#include - -class TestTree : public QObject -{ - Q_OBJECT - -private slots: - void initTestCase(); - void cleanupTestCase(); - void testOpenFile_invalid(); -}; - -void TestTree::initTestCase() -{ - qDebug() << "Initializing TestTree tests..."; -} - -void TestTree::cleanupTestCase() -{ - qDebug() << "Cleaning up TestTree tests..."; -} - -void TestTree::testOpenFile_invalid() -{ - QSplitter *splitter = new QSplitter; - Tree tree(splitter); - - QModelIndex index; - - tree.openFile(index); - - QVERIFY2(FileManager::getInstance().getCurrentFileName().isEmpty(), "FileManager should not process an invalid file."); -} - -QTEST_MAIN(TestTree) -#include "test_tree.moc"