diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ca0e385 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. MacOS, WSL, Windows] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..58cada2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,34 @@ +--- +name: Feature request +about: Suggest and idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## Feature Request + +### Description +_A brief and clear description of the feature request._ + +### Motivation +_Why is this feature needed? What problem does it solve?_ + +### Tasks +List possible implementation steps (use checkboxes for progress tracking): +- [ ] Task 1 +- [ ] Task 2 +- [ ] Task 3 + +### Alternatives Considered +_Have you considered any alternative solutions? If so, explain why this approach is preferred._ + +### Acceptance Criteria +_Define what conditions must be met for the feature to be considered complete._ + +### Additional Context +_Add relevant links, screenshots, or other information that would help understand the request._ + +> [!TIP] +> Add any related Labels to this issue. diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml new file mode 100644 index 0000000..9af8448 --- /dev/null +++ b/.github/workflows/cpp.yml @@ -0,0 +1,90 @@ +name: C++ CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] # TO-DO: Add back windows-latest when the project is tested on a Windows machine. + fail-fast: false + + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + # Set up Python 3.10 or later + - name: Set up Python 3.10 (Windows) + if: matrix.os == 'windows-latest' + uses: actions/setup-python@v2 + with: + python-version: '3.10' + + # Install C++ Compiler & Build Tools + - name: Set up C++ environment (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y g++ make cmake libyaml-cpp-dev + g++ --version + cmake --version + make --version + + - name: Set up C++ environment (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install make cmake yaml-cpp + echo 'export PATH="/usr/local/opt/gcc/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + g++ --version + cmake --version + make --version + + - name: Set up C++ environment (Windows) + if: matrix.os == 'windows-latest' + run: | + choco install mingw --version=8.1.0-1 + choco install make cmake + echo C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo C:\ProgramData\chocolatey\lib\cmake\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + g++ --version + cmake --version + make --version + + # Install Qt6 + - name: Install Qt6 (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get install -y qt6-base-dev + echo 'export PATH="/usr/lib/qt6/bin:$PATH"' >> ~/.bashrc + source ~/.bashrc + + - name: Install Qt6 (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install qt6 + echo 'export PATH="/opt/homebrew/opt/qt6/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + + - name: Install Qt6 (Windows) + if: matrix.os == 'windows-latest' + run: | + python -m pip install aqtinstall + python -m aqt install-qt windows desktop 6.6.0 win64_mingw --outputdir C:\Qt + echo C:\Qt\6.6.0\mingw_64\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + # Build the Project + - name: Build project + run: make build + + # Install the Project + - name: Install project + run: | + cd ${{ github.workspace }} + make install \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3620365 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,95 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + native: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] # TODO: Add windows-latest when officially supported + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + # Set up Python 3.10 or later + - name: Set up Python 3.10 (Windows) + if: matrix.os == 'windows-latest' + uses: actions/setup-python@v2 + with: + python-version: '3.10' + + # Install C++ Compiler & Build Tools + - name: Set up C++ environment (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y g++ make cmake libyaml-cpp-dev + g++ --version + cmake --version + make --version + + - name: Set up C++ environment (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install make cmake yaml-cpp + echo 'export PATH="/usr/local/opt/gcc/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + g++ --version + cmake --version + make --version + + - name: Set up C++ environment (Windows) + if: matrix.os == 'windows-latest' + run: | + choco install mingw --version=8.1.0-1 + choco install make cmake + echo C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo C:\ProgramData\chocolatey\lib\cmake\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + g++ --version + cmake --version + make --version + + # Install Qt6 + - name: Install Qt6 (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get install -y qt6-base-dev + echo 'export PATH="/usr/lib/qt6/bin:$PATH"' >> ~/.bashrc + source ~/.bashrc + + - name: Install Qt6 (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install qt6 + echo 'export PATH="/opt/homebrew/opt/qt6/bin:$PATH"' >> ~/.bash_profile + source ~/.bash_profile + + - name: Install Qt6 (Windows) + if: matrix.os == 'windows-latest' + run: | + python -m pip install aqtinstall + python -m aqt install-qt windows desktop 6.6.0 win64_mingw --outputdir C:\Qt + echo C:\Qt\6.6.0\mingw_64\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + # Build the Project + - name: Build project + run: make build + + - name: Run Unit Tests + run: make test + + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Run Docker Test + run: | + chmod +x ./run_docker.sh + ./run_docker.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index bc4f222..389bf77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,90 +1,35 @@ cmake_minimum_required(VERSION 3.16) -# Project name -set(TARGET_NAME CodeAstra) - -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) +project(CodeAstra VERSION 0.1.0 DESCRIPTION "Code Editor written in modern C++ using Qt6") -# 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}) +# 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}") - -# 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() - -# Set Qt path for find_package -set(CMAKE_PREFIX_PATH ${Qt_DIR}) - -# Find Qt Widgets -find_package(Qt${QT_MAJOR_VERSION} COMPONENTS Widgets REQUIRED) +# Enable Qt tools +set(CMAKE_AUTOMOC ON) -# Add executable and source files -add_executable(${TARGET_NAME} - src/main.cpp - src/MainWindow.cpp - src/CodeEditor.cpp - src/Syntax.cpp - src/Tree.cpp - include/MainWindow.h - include/CodeEditor.h - include/Syntax.h - include/Tree.h -) +# Define target names +set(TARGET_NAME CodeAstra) +set(EXECUTABLE_NAME ${TARGET_NAME}App) -qt_add_resources(APP_RESOURCES resources.qrc) -target_sources(${TARGET_NAME} PRIVATE ${APP_RESOURCES}) +# Set Qt version +set(QT_MAJOR_VERSION 6) -# Compiler flags per OS -if(MSVC) - target_compile_options(${TARGET_NAME} PRIVATE /W4 /WX) -elseif(APPLE) - target_compile_options(${TARGET_NAME} PRIVATE -Wall -Wextra -pedantic -Werror) - set_target_properties(${TARGET_NAME} PROPERTIES MACOSX_BUNDLE TRUE) -else() - target_compile_options(${TARGET_NAME} PRIVATE -Wall -Wextra -pedantic -Werror) -endif() +# Find Qt +find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS Core Widgets Test) -# Include directories -target_include_directories(${TARGET_NAME} PRIVATE ${Qt${QT_MAJOR_VERSION}_INCLUDE_DIRS}) -target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include) +# yaml-cpp +find_package(yaml-cpp REQUIRED CONFIG) -# Set output names properly for Debug and Release -set_target_properties(${TARGET_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}" - DEBUG_OUTPUT_NAME "${TARGET_NAME}d" - RELEASE_OUTPUT_NAME ${TARGET_NAME} -) +# Add subdirectories +add_subdirectory(src) +add_subdirectory(tests) -# Link necessary Qt libraries -target_link_libraries(${TARGET_NAME} PRIVATE Qt${QT_MAJOR_VERSION}::Widgets) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b138b66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM ubuntu:24.04 AS builder +WORKDIR / + +# Add dependencies +RUN apt-get update +RUN apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + g++ \ + cmake \ + make \ + mesa-common-dev \ + qt6-base-dev \ + x11-utils \ + x11-xserver-utils \ + xvfb \ + libyaml-cpp-dev +RUN rm -rf /var/lib/apt/lists/* + +# Copy CodeAstra into the container +COPY . /CodeAstra +WORKDIR /CodeAstra + +# Run dockerized tests script +RUN chmod +x ./test_with_xvbf.sh + +CMD ["./test_with_xvbf.sh"] diff --git a/Makefile b/Makefile index f990940..329778d 100644 --- a/Makefile +++ b/Makefile @@ -1,77 +1,39 @@ PROJECT = CodeAstra -BUILD_DIR = build -EXECUTABLE = $(PROJECT) +BUILD_DIR = $(PWD)/build -# 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 the software..." - @rm -rf $(EXECUTABLE).app $(EXECUTABLE)d.app - @OS_NAME=$(shell uname -s 2>/dev/null || echo "Windows"); \ - if [ "$$OS_NAME" = "Darwin" ]; then \ - rm -rf "$(HOME)/Desktop/$(EXECUTABLE).app"; \ - echo "MacOS: Shortcut removed..."; \ - elif [ "$$OS_NAME" = "Linux" ] && grep -qi "microsoft" /proc/version 2>/dev/null; then \ - rm -rf "$(HOME)/Desktop/$(EXECUTABLE)"; \ - echo "WSL: Shortcut removed..."; \ - elif [ "$$OS_NAME" = "Linux" ]; then \ - rm -rf "$(HOME)/Desktop/$(EXECUTABLE)"; \ - echo "Linux: Shortcut removed..."; \ - elif echo "$$OS_NAME" | grep -qE "CYGWIN|MINGW|MSYS"; then \ - rm -f "$(USERPROFILE)/Desktop/$(EXECUTABLE).exe"; \ - echo "Cygwin/Mingw/MSYS: Shortcut removed..."; \ - elif [ "$$OS_NAME" = "Windows" ]; then \ - if [ -n "$$USERPROFILE" ]; then \ - cmd /c "if exist \"$$USERPROFILE\\Desktop\\$(EXECUTABLE).exe\" del /f /q \"$$USERPROFILE\\Desktop\\$(EXECUTABLE).exe\"" && echo "Windows: Shortcut removed..."; \ - else \ - echo "Windows: Could not determine user profile directory."; \ - fi \ - fi - -# Install the project -install: +install: build @echo "Installing $(PROJECT)..." - @cd $(BUILD_DIR) && make - @echo "Do you want to create a shortcut on the desktop? (Y/n)" - @read choice; \ - if [ "$$choice" = "y" ] || [ "$$choice" = "Y" ]; then \ - echo "Creating shortcut..."; \ - OS_NAME=$(shell uname -s); \ - if [ "$$OS_NAME" = "Darwin" ]; then \ - echo "MacOS Detected..."; \ - cp -R $(EXECUTABLE).app ~/Desktop/; \ - elif [ "$$OS_NAME" = "Linux" ] && grep -qi "microsoft" /proc/version 2>/dev/null; then \ - echo "WSL Detected..."; \ - cp $(EXECUTABLE) ~/Desktop/; \ - elif [ "$$OS_NAME" = "Linux" ]; then \ - echo "Linux Detected..."; \ - cp $(EXECUTABLE) ~/Desktop/; \ - elif echo "$$OS_NAME" | grep -qE "CYGWIN|MINGW|MSYS"; then \ - echo "Windows-like Environment Detected (Cygwin/MSYS)..."; \ - cp $(EXECUTABLE).exe "$$USERPROFILE/Desktop/"; \ - elif [ "$$OS_NAME" = "Windows" ]; then \ - echo "Native Windows Detected..."; \ - if [ -n "$$USERPROFILE" ]; then \ - cp $(EXECUTABLE).exe "$$USERPROFILE/Desktop/"; \ - else \ - echo "Windows: Could not determine user profile directory."; \ - fi \ - fi \ - fi - @echo "$(PROJECT) installed." + @cmake --build $(BUILD_DIR) + @echo "Installation complete." + +build_tests: build + @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 8f868f0..22ba0b4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,28 @@ CodeAstra Logo

+
+ + + [![C++ CI](https://github.com/sandbox-science/CodeAstra/actions/workflows/cpp.yml/badge.svg)](https://github.com/sandbox-science/CodeAstra/actions/workflows/cpp.yml) + [![Test CI](https://github.com/sandbox-science/CodeAstra/actions/workflows/test.yml/badge.svg)](https://github.com/sandbox-science/CodeAstra/actions/workflows/test.yml) + + + + CodeAstra Latest Release + + + + + C++ Version 20+ + + + + + Qt Version 6.8 + +
+

CodeAstra ~ Modern Code Editor

CodeAstra is a modern, extensible, and lightweight code editor built using C++ and Qt6, designed to offer a fast, customizable, and feature-rich development experience. Inspired by NeoVim and VSCode, it **will** provide efficient file navigation, syntax highlighting, and a powerful plugin system, making it an ideal choice for developers who need speed, flexibility, and control. With a focus on performance and usability, the editor **will** support split views, an integrated terminal, customizable key bindings, and seamless Git integration, catering to both beginners and power users. @@ -27,10 +49,18 @@ Please, check the [wiki](https://github.com/sandbox-science/CodeAstra/wiki) for - [x] Basic text editing - [x] Open a file - [x] Save file - - [ ] Create a new file -- [ ] File tree navigation -- [ ] Syntax highlighting + - [ ] Create a new file ~ in progress +- [x] File tree navigation +- [ ] Syntax highlighting ~ in progress + - Supported Languages: + - [x] Markdown (**foundation**) + - [x] YAML (**foundation**) + - [ ] C/C++ (**in progress**) + - [ ] Golang (**in progress**) + - [ ] Python (**Backlog**) + - [ ] Elixir (**Backlog**) + - more to come ... ([contribution welcomed](https://github.com/sandbox-science/CodeAstra/issues/4)) Read our [wiki](https://github.com/sandbox-science/CodeAstra/wiki/Config) for more information - [ ] 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/config/cpp.syntax.yaml b/config/cpp.syntax.yaml new file mode 100644 index 0000000..c44f15a --- /dev/null +++ b/config/cpp.syntax.yaml @@ -0,0 +1,38 @@ +extensions: [cpp, c, h, hpp] + +keywords: + keyword: + - regex: "\\b(char|class|const|double|enum|explicit|friend|inline|int|long|namespace|operator|private|protected|public|short|signals|signed|slots|static|struct|template|typedef|typename|union|unsigned|virtual|void|volatile|foreach)\\b" + color: "#003478" # Dark Blue + bold: true + - regex: "\\b(for|while|do|if|else)\\b" + color: "#D9001D" # Bright Red + - regex: "(?\\s+.*" + color: "#B48EAD" # Light Purple + italic: true + + todo: + - regex: "- \\[ \\]" + color: "#FF8C00" # Orange for unchecked tasks + - regex: "- \\[x\\]" + color: "#32CD32" # Green for completed tasks + - regex: "(?<=\\[)[^\\]]*(?=\\])" + color: "#E37100" \ No newline at end of file diff --git a/config/python.syntax.yaml b/config/python.syntax.yaml new file mode 100644 index 0000000..7c3fc58 --- /dev/null +++ b/config/python.syntax.yaml @@ -0,0 +1,24 @@ +extensions: [py] + +keywords: + keyword: + - regex: "\\bimport\\b" + color: "#FF1493" # Deep Pink + - regex: "\\bdef\\b" + color: "#800080" # Purple + - regex: "\\bclass\\b" + color: "#008000" # Green + bold: true + + comment: + - regex: "#[^\n]*" + color: "#808080" # Gray + italic: true + + string: + - regex: "\"(\\\\.|[^\"\\\\])*\"" + color: "#E37100" # Orange + + number: + - regex: "\\b\\d+\\b" + color: "#0000FF" # Blue diff --git a/config/yaml.syntax.yaml b/config/yaml.syntax.yaml new file mode 100644 index 0000000..25ff721 --- /dev/null +++ b/config/yaml.syntax.yaml @@ -0,0 +1,35 @@ +extensions: [yaml, yml] + +keywords: + comment: + - regex: "#[^\n]*" + color: "#336934" # Dark Green + italic: true + + keyword: + - regex: "\\[([^\\]]*)\\]" + color: "#DD9042" # Brownish Orange + - regex: "\\[|\\]" + color: "#E6B400" # Golden Yellow + + number: + - regex: "\\b[+-]?([0-9]*[.])?[0-9]+([eE][+-]?[0-9]+)?\\b" + color: "#BA68C8" # Purple + + boolean: + - regex: "\\b(true|false)\\b" + color: "#4CAF50" # Green + + null: + - regex: "\\b(null)\\b" + color: "#9E9E9E" # Gray + + key: + - regex: "(?<=^|\\s)([a-zA-Z0-9_-]+)(?=:)" + color: "#009688" # Teal + + string: + - regex: "\"(\\\\.|[^\"\\\\])*\"" + color: "#E37100" # Orange + - regex: "\\\\" + color: "#FFDE00" # Yellow \ No newline at end of file diff --git a/include/CodeEditor.h b/include/CodeEditor.h index e1f4d59..5ec9ab6 100644 --- a/include/CodeEditor.h +++ b/include/CodeEditor.h @@ -1,9 +1,18 @@ -#ifndef CODEEDITOR_H -#define CODEEDITOR_H +#pragma once #include #include +class FileManager; // Forward declaration + +/** + * @class CodeEditor + * @brief A custom code editor widget that extends QPlainTextEdit. + * + * The CodeEditor class provides a code editor with line number area, syntax highlighting, + * and basic editing modes (NORMAL and INSERT). It emits signals for status messages and + * handles key press and resize events. + */ class CodeEditor : public QPlainTextEdit { Q_OBJECT @@ -19,6 +28,10 @@ class CodeEditor : public QPlainTextEdit Mode mode = NORMAL; void lineNumberAreaPaintEvent(QPaintEvent *event); int lineNumberAreaWidth(); + void autoIndentation(); + +signals: + void statusMessageChanged(const QString &message); protected: void keyPressEvent(QKeyEvent *event) override; @@ -30,7 +43,11 @@ private slots: void updateLineNumberArea(const QRect &rect, int dy); private: - QWidget *lineNumberArea; -}; + QWidget *m_lineNumberArea; + FileManager *m_fileManager; -#endif // CODEEDITOR_H + void addLanguageSymbol(QTextCursor &cursor, const QString &commentSymbol); + void commentSelection(QTextCursor &cursor, const QString &commentSymbol); + void commentLine(QTextCursor &cursor, const QString &commentSymbol); + void addComment(); +}; \ No newline at end of file diff --git a/include/FileManager.h b/include/FileManager.h new file mode 100644 index 0000000..6a44b39 --- /dev/null +++ b/include/FileManager.h @@ -0,0 +1,72 @@ +#pragma once + +#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. + * + * The FileManager class is a singleton that handles multiple file-related operations + * within the application. It interacts with the CodeEditor and MainWindow classes + * to perform tasks such as creating new files, saving existing files, and opening + * files from the filesystem. The class ensures that only one instance of FileManager + * exists and provides a global point of access to it. + */ +class FileManager : public QObject +{ + Q_OBJECT + +public: + 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; + + QString getFileExtension() const; + QString getCurrentFileName() const; + + 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(); + void saveFileAs(); + void openFile(); + void loadFileInEditor(const QString &filePath); + + QString getDirectoryPath() const; + +private: + FileManager(CodeEditor *editor, MainWindow *mainWindow); + ~FileManager(); + + CodeEditor *m_editor; + MainWindow *m_mainWindow; + QSyntaxHighlighter *m_currentHighlighter = nullptr; + QString m_currentFileName; +}; \ No newline at end of file diff --git a/include/LineNumberArea.h b/include/LineNumberArea.h index ac052ef..1f2a85e 100644 --- a/include/LineNumberArea.h +++ b/include/LineNumberArea.h @@ -1,5 +1,4 @@ -#ifndef LINENUMBER_H -#define LINENUMBER_H +#pragma once #include "CodeEditor.h" @@ -7,24 +6,31 @@ #include #include +/** + * @class LineNumberArea + * @brief A widget that displays line numbers for the CodeEditor. + * + * The LineNumberArea class is a QWidget that is used to display line numbers + * alongside the CodeEditor widget. + * + * @note This class is intended to be used as a part of the CodeEditor widget. + */ class LineNumberArea : public QWidget { public: - LineNumberArea(CodeEditor *editor) : QWidget(editor), codeEditor(editor) {} + LineNumberArea(CodeEditor *editor) : QWidget(editor), codeEditor(editor) {} - QSize sizeHint() const override - { - return QSize(codeEditor->lineNumberAreaWidth(), 0); - } + QSize sizeHint() const override + { + return QSize(codeEditor->lineNumberAreaWidth(), 0); + } protected: - void paintEvent(QPaintEvent *event) override - { - codeEditor->lineNumberAreaPaintEvent(event); - } + void paintEvent(QPaintEvent *event) override + { + codeEditor->lineNumberAreaPaintEvent(event); + } private: - CodeEditor *codeEditor; -}; - -#endif // LINENUMBER_H \ No newline at end of file + CodeEditor *codeEditor; +}; \ No newline at end of file diff --git a/include/MainWindow.h b/include/MainWindow.h index 81bcf80..30c5a8c 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -1,45 +1,51 @@ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include "CodeEditor.h" -#include "Syntax.h" -#include "Tree.h" - -#include -#include -#include -#include -#include -#include - -class MainWindow : public QMainWindow -{ - Q_OBJECT - -public: - explicit MainWindow(QWidget *parent = nullptr); - virtual ~MainWindow(); - void loadFileInEditor(const QString &filePath); - -private slots: - void newFile(); - void openFile(); - void saveFile(); - void saveFileAs(); - void showAbout(); - -private: - void createMenuBar(); - void createFileActions(QMenu *fileMenu); - void createHelpActions(QMenu *helpMenu); - void createAppActions(QMenu *appMenu); - QAction *createAction(const QIcon &icon, const QString &text, - const QKeySequence &shortcut, const QString &statusTip, - void (MainWindow::*slot)()); - CodeEditor *editor; - QString currentFileName; - Syntax *syntax; - Tree *tree; -}; - -#endif // MAINWINDOW_H +#pragma once + +#include +#include +#include +#include +#include +#include + +class CodeEditor; +class Syntax; +class Tree; +class FileManager; + +/** + * @class MainWindow + * @brief The MainWindow class represents the main UI window of the application. + * + * This class is responsible for initializing and managing the main components + * of the application, including the file tree view, code editor, and menu bar. + */ +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + virtual ~MainWindow(); + + // Initialize the file tree view and set it as the central widget + // of the main window, alongside the code editor + void initTree(); + + QAction *createAction(const QIcon &icon, const QString &text, + const QKeySequence &shortcut, const QString &statusTip, + const std::function &slot); + +private slots: + void showAbout(); + +private: + void createMenuBar(); + void createFileActions(QMenu *fileMenu); + void createHelpActions(QMenu *helpMenu); + void createAppActions(QMenu *appMenu); + + std::unique_ptr m_editor; + std::unique_ptr m_tree; + + FileManager *m_fileManager; +}; \ No newline at end of file diff --git a/include/Syntax.h b/include/Syntax.h index 4b40f5a..25eb89c 100644 --- a/include/Syntax.h +++ b/include/Syntax.h @@ -1,37 +1,63 @@ -#ifndef SYNTAX_H -#define SYNTAX_H +#pragma once +#include #include #include -#include -#include - +#include + +/** + * @class Syntax + * @brief A custom syntax highlighter class that extends QSyntaxHighlighter to provide + * syntax highlighting functionality based on user-defined rules. + * + * This class allows you to define syntax highlighting rules using regular expressions + * and associated text formats. It applies these rules to text blocks to highlight + * specific patterns. + * + * @note This class inherits the constructor from QSyntaxHighlighter. + */ class Syntax : public QSyntaxHighlighter { Q_OBJECT public: - Syntax(QTextDocument *parent = nullptr); + Syntax(QTextDocument *parent, const YAML::Node &config); + ~Syntax() = default; protected: + /** + * @brief Highlights the given text block based on the defined syntax rules. + * + * @param text The text block to be highlighted. + */ void highlightBlock(const QString &text) override; -private: +public: + /** + * @struct SyntaxRule + * @brief Represents a single syntax highlighting rule. + * + * A syntax rule consists of a regular expression pattern and a text format + * to apply to matching text. + */ struct SyntaxRule { - QRegularExpression pattern; - QTextCharFormat format; + QRegularExpression m_pattern; + QTextCharFormat m_format; }; - QList syntaxRules; - - QTextCharFormat keywordFormat; - QTextCharFormat singleLineCommentFormat; - QTextCharFormat quotationMark; - QTextCharFormat functionFormat; - QTextCharFormat parenthesisFormat; - QTextCharFormat charFormat; + QVector m_syntaxRules; + + /** + * @brief Adds a new syntax highlighting rule. + * + * This method allows you to define a new rule by specifying a regular expression + * pattern and the corresponding text format. + * + * @param pattern The regular expression pattern for the rule. + * @param format The text format to apply to matches of the pattern. + */ void addPattern(const QString &pattern, const QTextCharFormat &format); -}; -#endif // SYNTAX_H \ No newline at end of file + void loadSyntaxRules(const YAML::Node &config); +}; \ No newline at end of file diff --git a/include/SyntaxManager.h b/include/SyntaxManager.h new file mode 100644 index 0000000..83c7234 --- /dev/null +++ b/include/SyntaxManager.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Syntax.h" + +#include +#include +#include + +#include + +/** + * @class SyntaxManager + * @brief Manages the creation of syntax highlighters for different file types. + * + * The SyntaxManager class provides functionality to create syntax highlighters + * based on file extensions. It supports dynamic creation of highlighters by + * utilizing configuration data and applies them to QTextDocument objects. + * + * @note This class is designed to work with the Qt framework and YAML configuration. + */ +class SyntaxManager +{ +public: + /** + * @brief Creates a syntax highlighter based on the file extension. + * @param extension The file extension (e.g., "cpp", "py"). + * @param doc The QTextDocument to which the highlighter will be applied. + * @return A unique pointer to the appropriate syntax highlighter, or nullptr if not available. + */ + static std::unique_ptr createSyntaxHighlighter(const QString &extension, QTextDocument *doc); + +private: + static std::unique_ptr createHighlighter(QTextDocument *doc, const std::vector &config, const QString &extension); +}; diff --git a/include/Tree.h b/include/Tree.h index 9584606..856e71f 100644 --- a/include/Tree.h +++ b/include/Tree.h @@ -1,31 +1,45 @@ -#ifndef TREE_H -#define TREE_H +#pragma once + +#include "FileManager.h" #include -#include -#include #include +#include +#include -class MainWindow; // Forward declaration +// Forward declarations +class QTreeView; +class QFileSystemModel; +class QFileIconProvider; +/** + * @class Tree + * @brief A class that represents a tree view for displaying the file system. + * + * The Tree class is responsible for creating and managing a tree view that displays + * the file system. + */ class Tree : public QObject { Q_OBJECT public: - Tree(QSplitter *splitter, MainWindow *mainWindow); + explicit Tree(QSplitter *splitter); ~Tree(); -private: - void showContextMenu(const QPoint &pos); - void setupModel(); + void initialize(const QString &directory); + void setupModel(const QString &directory); void setupTree(); void openFile(const QModelIndex &index); - QString getDirectoryPath(); - QFileSystemModel *model; - QTreeView *tree; - MainWindow *mainWindow; -}; + QFileSystemModel* getModel() const; + +private: + void showContextMenu(const QPoint &pos); + QFileInfo getPathInfo(); + void isSuccessful(OperationResult result); -#endif // TREE_H + std::unique_ptr m_iconProvider; + std::unique_ptr m_model; + std::unique_ptr m_tree; +}; \ No newline at end of file diff --git a/run_docker.sh b/run_docker.sh new file mode 100755 index 0000000..2aa18f5 --- /dev/null +++ b/run_docker.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +docker build -t "code-astra:tester" . +docker run --name "code-astra-tester" "code-astra:tester" +docker rm "code-astra-tester" 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 4259f81..63815f9 100644 --- a/src/CodeEditor.cpp +++ b/src/CodeEditor.cpp @@ -1,14 +1,18 @@ #include "CodeEditor.h" #include "MainWindow.h" #include "LineNumberArea.h" +#include "FileManager.h" #include #include +#include +#include -CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent) +CodeEditor::CodeEditor(QWidget *parent) + : QPlainTextEdit(parent), + m_lineNumberArea(new LineNumberArea(this)), + m_fileManager(&FileManager::getInstance()) { - lineNumberArea = new LineNumberArea(this); - connect(this, &CodeEditor::blockCountChanged, this, &CodeEditor::updateLineNumberAreaWidth); connect(this, &CodeEditor::updateRequest, this, &CodeEditor::updateLineNumberArea); connect(this, &CodeEditor::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine); @@ -26,13 +30,14 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) moveCursor(QTextCursor::WordLeft, QTextCursor::KeepAnchor); return; } - + if (mode == NORMAL) { switch (event->key()) { case Qt::Key_I: mode = INSERT; + emit statusMessageChanged("Insert mode activated"); break; case Qt::Key_A: moveCursor(QTextCursor::Left); @@ -46,14 +51,160 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) case Qt::Key_W: moveCursor(QTextCursor::Up); break; - case Qt::Key_Escape: + default: + emit statusMessageChanged("Insert mode is not active. Press 'i' to enter insert mode."); + break; + } + } + + else if (mode == INSERT) + { + if (event->key() == Qt::Key_Escape) + { 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); + } + } +} + +// 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()) + { + commentSelection(cursor, commentSymbol); + } + else + { + commentLine(cursor, commentSymbol); + } +} + +// Comment/uncomment the selected text or the current line +void CodeEditor::commentSelection(QTextCursor &cursor, const QString &commentSymbol) +{ + int start = cursor.selectionStart(); + int end = cursor.selectionEnd(); + + cursor.setPosition(start); + int startBlockNumber = cursor.blockNumber(); + cursor.setPosition(end); + int endBlockNumber = cursor.blockNumber(); + + cursor.setPosition(start); + for (int i = startBlockNumber; i <= endBlockNumber; ++i) + { + cursor.movePosition(QTextCursor::StartOfLine); + QString lineText = cursor.block().text(); + + if (lineText.startsWith(commentSymbol)) + { + cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, commentSymbol.length() + 1); + cursor.removeSelectedText(); + } + else + { + cursor.insertText(commentSymbol + " "); + } + + cursor.movePosition(QTextCursor::NextBlock); + } +} + +// Comment/uncomment the single current line +void CodeEditor::commentLine(QTextCursor &cursor, const QString &commentSymbol) +{ + cursor.select(QTextCursor::LineUnderCursor); + QString lineText = cursor.selectedText(); + + if (lineText.startsWith(commentSymbol)) + { + lineText.remove(0, commentSymbol.length() + 1); + } + else + { + lineText.prepend(commentSymbol + " "); + } + + cursor.insertText(lineText); +} + +void CodeEditor::addComment() +{ + QTextCursor cursor = textCursor(); + QString fileExtension = m_fileManager->getFileExtension(); + qDebug() << "File Extension:" << fileExtension; + + if (fileExtension == "cpp" || fileExtension == "h" || + fileExtension == "hpp" || fileExtension == "c" || + fileExtension == "java" || fileExtension == "go" || + fileExtension == "json") + { + addLanguageSymbol(cursor, "//"); + } + else if (fileExtension == "py" || fileExtension == "yaml" || + fileExtension == "yml" || fileExtension == "sh" || + fileExtension == "bash") + { + addLanguageSymbol(cursor, "#"); + } + else if (fileExtension == "sql") + { + addLanguageSymbol(cursor, "--"); + } else { - QPlainTextEdit::keyPressEvent(event); + qDebug() << "Unsupported file extension for commenting."; } } @@ -82,11 +233,11 @@ void CodeEditor::updateLineNumberArea(const QRect &rect, int dy) { if (dy) { - lineNumberArea->scroll(0, dy); + m_lineNumberArea->scroll(0, dy); } else { - lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height()); + m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height()); } if (rect.contains(viewport()->rect())) @@ -100,7 +251,7 @@ void CodeEditor::resizeEvent(QResizeEvent *e) QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); - lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); + m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } void CodeEditor::highlightCurrentLine() @@ -128,13 +279,13 @@ void CodeEditor::highlightCurrentLine() void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) { - QPainter painter(lineNumberArea); + QPainter painter(m_lineNumberArea); // Match the background color of the editor painter.fillRect(event->rect(), palette().color(QPalette::Base)); // Draw a separating line between the number area and the text editor - int separatorX = lineNumberArea->width() - 4; + int separatorX = m_lineNumberArea->width() - 4; painter.drawLine(separatorX, event->rect().top(), separatorX, event->rect().bottom()); QTextBlock block = firstVisibleBlock(); @@ -152,7 +303,7 @@ void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) QString number = QString::number(blockNumber + 1); painter.setPen(Qt::darkGray); - painter.drawText(0, top + padding, lineNumberArea->width(), lineHeight, + painter.drawText(0, top + padding, m_lineNumberArea->width(), lineHeight, Qt::AlignCenter, number); } diff --git a/src/FileManager.cpp b/src/FileManager.cpp new file mode 100644 index 0000000..7d35e1b --- /dev/null +++ b/src/FileManager.cpp @@ -0,0 +1,352 @@ +#include "FileManager.h" +#include "CodeEditor.h" +#include "MainWindow.h" +#include "SyntaxManager.h" + +#include +#include +#include +#include +#include +#include +#include + +FileManager::FileManager(CodeEditor *editor, MainWindow *mainWindow) + : m_editor(editor), m_mainWindow(mainWindow) +{ + qDebug() << "FileManager initialized."; +} + +FileManager::~FileManager() {} + +void FileManager::initialize(CodeEditor *editor, MainWindow *mainWindow) +{ + m_editor = editor; + m_mainWindow = mainWindow; +} + +QString FileManager::getCurrentFileName() const +{ + return m_currentFileName; +} + +void FileManager::setCurrentFileName(const QString fileName) +{ + m_currentFileName = fileName; +} + +void FileManager::newFile() +{ + // Logic to create a new file +} + +void FileManager::saveFile() +{ + if (m_currentFileName.isEmpty()) + { + saveFileAs(); + return; + } + + qDebug() << "Saving file:" << m_currentFileName; + + QFile file(m_currentFileName); + if (!file.open(QFile::WriteOnly | QFile::Text)) + { + QMessageBox::warning(nullptr, "Error", "Cannot save file: " + file.errorString()); + return; + } + + QTextStream out(&file); + if (m_editor) + { + out << m_editor->toPlainText(); + } + else + { + QMessageBox::critical(nullptr, "Error", "Editor is not initialized."); + return; + } + file.close(); + + emit m_editor->statusMessageChanged("File saved successfully."); +} + +void FileManager::saveFileAs() +{ + QString fileExtension = getFileExtension(); + QString filter = "All Files (*);;C++ Files (*.cpp *.h);;Text Files (*.txt)"; + if (!fileExtension.isEmpty()) + { + filter = QString("%1 Files (*.%2);;%3").arg(fileExtension.toUpper(), fileExtension, filter); + } + + QString fileName = QFileDialog::getSaveFileName(nullptr, "Save File As", QString(), filter); + + if (!fileName.isEmpty()) + { + m_currentFileName = fileName; + saveFile(); + } +} + +void FileManager::openFile() +{ + QString fileName = QFileDialog::getOpenFileName(nullptr, "Open File", QString(), + "All Files (*);;C++ Files (*.cpp *.h);;Text Files (*.txt)"); + if (!fileName.isEmpty()) + { + qDebug() << "Opening file: " << fileName; + m_currentFileName = fileName; + loadFileInEditor(fileName); + } + else + { + qDebug() << "No file selected."; + } +} + +void FileManager::loadFileInEditor(const QString &filePath) +{ + qDebug() << "Loading file:" << filePath; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QMessageBox::warning(nullptr, "Error", "Cannot open file: " + file.errorString()); + return; + } + + QTextStream in(&file); + if (m_editor) + { + m_editor->setPlainText(in.readAll()); + + delete m_currentHighlighter; + + // Create and assign a new syntax highlighter based on language extension + m_currentHighlighter = SyntaxManager::createSyntaxHighlighter(getFileExtension(), m_editor->document()).release(); + } + else + { + QMessageBox::critical(nullptr, "Error", "Editor is not initialized."); + return; + } + file.close(); + + if (m_mainWindow) + { + m_mainWindow->setWindowTitle("CodeAstra ~ " + QFileInfo(filePath).fileName()); + } + else + { + qWarning() << "MainWindow is not initialized in FileManager."; + } +} + +QString FileManager::getFileExtension() const +{ + if (m_currentFileName.isEmpty()) + { + qDebug() << "Error: No File name set!"; + return QString(); + } + + return QFileInfo(m_currentFileName).suffix().toLower(); +} + +QString FileManager::getDirectoryPath() const +{ + return QFileDialog::getExistingDirectory( + 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/MainWindow.cpp b/src/MainWindow.cpp index 23e3a83..1bde3a0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,51 +1,72 @@ #include "MainWindow.h" -#include "Syntax.h" #include "Tree.h" +#include "CodeEditor.h" +#include "FileManager.h" #include #include -#include -#include #include #include #include #include -#include -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), + m_editor(std::make_unique(this)), + m_tree(nullptr), + m_fileManager(&FileManager::getInstance()) { + m_fileManager->initialize(m_editor.get(), this); setWindowTitle("CodeAstra ~ Code Editor"); - editor = new CodeEditor(this); - syntax = new Syntax(editor->document()); + connect(m_editor.get(), &CodeEditor::statusMessageChanged, this, [this](const QString &message) + { + QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); + statusBar()->showMessage("[" + timestamp + "] " + message, 4000); + }); - QFontMetrics metrics(editor->font()); + // Set tab width to 4 spaces + QFontMetrics metrics(m_editor->font()); int spaceWidth = metrics.horizontalAdvance(" "); - editor->setTabStopDistance(spaceWidth * 4); + m_editor->setTabStopDistance(spaceWidth * 4); + m_editor->setLineWrapMode(QPlainTextEdit::NoWrap); + + initTree(); + createMenuBar(); + showMaximized(); +} + +MainWindow::~MainWindow() {} +void MainWindow::initTree() +{ QSplitter *splitter = new QSplitter(Qt::Horizontal, this); setCentralWidget(splitter); - tree = new Tree(splitter, this); + m_tree = std::make_unique(splitter); + splitter->addWidget(m_editor.get()); splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); splitter->setHandleWidth(5); - splitter->setSizes(QList() << 20 << 950); - splitter->addWidget(editor); - - createMenuBar(); - showMaximized(); + splitter->setSizes(QList() << 150 << 800); + splitter->setStretchFactor(0, 1); + splitter->setStretchFactor(1, 3); + splitter->setChildrenCollapsible(false); + splitter->setOpaqueResize(true); } -MainWindow::~MainWindow() {} - void MainWindow::createMenuBar() { QMenuBar *menuBar = new QMenuBar(this); QMenu *fileMenu = menuBar->addMenu("File"); + fileMenu->setObjectName("File"); + QMenu *helpMenu = menuBar->addMenu("Help"); - QMenu *appMenu = menuBar->addMenu("CodeAstra"); + helpMenu->setObjectName("Help"); + + QMenu *appMenu = menuBar->addMenu("CodeAstra"); + appMenu->setObjectName("CodeAstra"); createFileActions(fileMenu); createHelpActions(helpMenu); @@ -56,24 +77,30 @@ void MainWindow::createMenuBar() void MainWindow::createFileActions(QMenu *fileMenu) { - QAction *newAction = createAction(QIcon::fromTheme("document-new"), tr("&New File..."), QKeySequence::New, tr("Create a new file"), &MainWindow::newFile); - fileMenu->addAction(newAction); - - QAction *openAction = createAction(QIcon::fromTheme("document-open"), tr("&Open..."), QKeySequence::Open, tr("Open an existing file"), &MainWindow::openFile); - fileMenu->addAction(openAction); - - QAction *saveAction = createAction(QIcon::fromTheme("document-save"), tr("&Save"), QKeySequence::Save, tr("Save your file"), &MainWindow::saveFile); - fileMenu->addAction(saveAction); - - QAction *saveAsAction = createAction(QIcon::fromTheme("document-saveAs"), tr("&Save As"), QKeySequence::SaveAs, tr("Save current file as..."), &MainWindow::saveFileAs); - fileMenu->addAction(saveAsAction); + fileMenu->addAction(createAction(QIcon(), tr("&New"), QKeySequence::New, tr("Create a new file"), [this]() { m_fileManager->newFile(); })); + fileMenu->addSeparator(); + fileMenu->addAction(createAction(QIcon(), tr("&Open &Project"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_O), tr("Open a project"), [this]() + { + QString projectPath = m_fileManager->getDirectoryPath(); + if (!projectPath.isEmpty()) + { + m_tree->initialize(projectPath); + } + })); + fileMenu->addAction(createAction(QIcon(), tr("&Open"), QKeySequence::Open, tr("Open an existing file"), [this]() { m_fileManager->openFile(); })); + fileMenu->addSeparator(); + fileMenu->addAction(createAction(QIcon(), tr("&Save"), QKeySequence::Save, tr("Save the current file"), [this]() { m_fileManager->saveFile(); })); + fileMenu->addAction(createAction(QIcon(), tr("Save &As"), QKeySequence::SaveAs, tr("Save the file with a new name"), [this]() { m_fileManager->saveFileAs(); })); } void MainWindow::createHelpActions(QMenu *helpMenu) { QAction *helpDoc = new QAction(tr("Documentation"), this); connect(helpDoc, &QAction::triggered, this, []() - { QDesktopServices::openUrl(QUrl("https://github.com/sandbox-science/CodeAstra/wiki")); }); + { + QDesktopServices::openUrl(QUrl("https://github.com/sandbox-science/CodeAstra/wiki")); + }); + helpDoc->setStatusTip(tr("Open Wiki")); helpMenu->addAction(helpDoc); } @@ -85,7 +112,7 @@ void MainWindow::createAppActions(QMenu *appMenu) appMenu->addAction(aboutAction); } -QAction *MainWindow::createAction(const QIcon &icon, const QString &text, const QKeySequence &shortcut, const QString &statusTip, void (MainWindow::*slot)()) +QAction *MainWindow::createAction(const QIcon &icon, const QString &text, const QKeySequence &shortcut, const QString &statusTip, const std::function &slot) { QAction *action = new QAction(icon, text, this); @@ -96,11 +123,6 @@ QAction *MainWindow::createAction(const QIcon &icon, const QString &text, const return action; } -void MainWindow::newFile() -{ - // TO-DO: Implement new file function -} - void MainWindow::showAbout() { // Extract the C++ version from the __cplusplus macro @@ -143,86 +165,5 @@ void MainWindow::showAbout() QString::number((QT_VERSION >> 8) & 0xFF) + "." + // Minor version QString::number(QT_VERSION & 0xFF)); // Patch version - QMessageBox::about(this, "About Code Astra", aboutText); -} - -void MainWindow::openFile() -{ - QString fileName = QFileDialog::getOpenFileName(this, "Open File"); - if (!fileName.isEmpty()) - { - QFile file(fileName); - if (!file.open(QFile::ReadOnly | QFile::Text)) - { - QMessageBox::warning(this, "Error", "Cannot open file: " + file.errorString()); - return; - } - - QTextStream in(&file); - if (editor) - { - editor->setPlainText(in.readAll()); - } - else - { - QMessageBox::critical(this, "Error", "Editor is not initialized."); - } - file.close(); - - currentFileName = fileName; - - setWindowTitle("CodeAstra ~ " + QFileInfo(fileName).fileName()); - } -} - -void MainWindow::saveFile() -{ - if (currentFileName.isEmpty()) - { - saveFileAs(); - return; - } - - QFile file(currentFileName); - if (!file.open(QFile::WriteOnly | QFile::Text)) - { - QMessageBox::warning(this, "Error", "Cannot save file: " + file.errorString()); - return; - } - - QTextStream out(&file); - if (editor) - { - out << editor->toPlainText(); - } - file.close(); - - statusBar()->showMessage("File saved successfully.", 2000); -} - -void MainWindow::saveFileAs() -{ - QString fileName = QFileDialog::getSaveFileName(this, "Save File As"); - if (!fileName.isEmpty()) - { - currentFileName = fileName; - saveFile(); - } -} - -void MainWindow::loadFileInEditor(const QString &filePath) -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) - { - QMessageBox::warning(this, "Error", "Cannot open file: " + file.errorString()); - return; - } - - QTextStream in(&file); - editor->setPlainText(in.readAll()); - file.close(); - - currentFileName = filePath; - setWindowTitle("CodeAstra ~ " + QFileInfo(filePath).fileName()); + QMessageBox::about(this, tr("About"), aboutText); } diff --git a/src/Syntax.cpp b/src/Syntax.cpp index 553c927..e3aab1c 100644 --- a/src/Syntax.cpp +++ b/src/Syntax.cpp @@ -1,66 +1,99 @@ #include "Syntax.h" -Syntax::Syntax(QTextDocument *parent) : QSyntaxHighlighter(parent) +Syntax::Syntax(QTextDocument *parent, const YAML::Node &config) + : QSyntaxHighlighter(parent) { - keywordFormat.setForeground(Qt::blue); - keywordFormat.setFontWeight(QFont::Bold); - QStringList keywordPatterns; - keywordPatterns << "\\bchar\\b" << "\\bclass\\b" << "\\bconst\\b" - << "\\bdouble\\b" << "\\benum\\b" << "\\bexplicit\\b" - << "\\bfriend\\b" << "\\binline\\b" << "\\bint\\b" - << "\\blong\\b" << "\\bnamespace\\b" << "\\boperator\\b" - << "\\bprivate\\b" << "\\bprotected\\b" << "\\bpublic\\b" - << "\\bshort\\b" << "\\bsignals\\b" << "\\bsigned\\b" - << "\\bslots\\b" << "\\bstatic\\b" << "\\bstruct\\b" - << "\\btemplate\\b" << "\\btypedef\\b" << "\\btypename\\b" - << "\\bunion\\b" << "\\bunsigned\\b" << "\\bvirtual\\b" - << "\\bvoid\\b" << "\\bvolatile\\b" << "\\bforeach\\b"; - foreach (const QString &pattern, keywordPatterns) - { - addPattern(pattern, keywordFormat); - } - - // Single line comment format expression - singleLineCommentFormat.setForeground(Qt::darkGray); - addPattern("//[^\n]*", singleLineCommentFormat); - - // Double quotation mark for string - quotationMark.setForeground(Qt::darkGreen); - addPattern("\".*\"", quotationMark); - - // Function format expression - functionFormat.setFontItalic(true); - functionFormat.setForeground(Qt::darkYellow); - addPattern("\\b[a-zA-Z_][a-zA-Z0-9_]*(?=\\s*\\()", functionFormat); - - // Color pattern for parenthesis - QColor parenthesisColor("#6495ED"); - parenthesisFormat.setForeground(parenthesisColor); - addPattern("[()]", parenthesisFormat); + qDebug() << "Syntax highlighter created"; + loadSyntaxRules(config); +} - // Regex for single character format 'a', '\n', etc - charFormat.setForeground(Qt::darkCyan); - addPattern("'(\\\\.|[^'])'", charFormat); +void Syntax::highlightBlock(const QString &text) +{ + for (const SyntaxRule &rule : m_syntaxRules) + { + QRegularExpressionMatchIterator matchIterator = rule.m_pattern.globalMatch(text); + while (matchIterator.hasNext()) + { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.m_format); + } + } } -// Add syntax highlighting patterns void Syntax::addPattern(const QString &pattern, const QTextCharFormat &format) { SyntaxRule rule; - rule.pattern = QRegularExpression(pattern); - rule.format = format; - syntaxRules.append(rule); + rule.m_pattern = QRegularExpression(pattern); + rule.m_format = format; + m_syntaxRules.append(rule); } -void Syntax::highlightBlock(const QString &text) +void Syntax::loadSyntaxRules(const YAML::Node &config) { - foreach (const SyntaxRule &rule, syntaxRules) - { - QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); - while (matchIterator.hasNext()) + m_syntaxRules.clear(); + + if (!config["keywords"]) { - QRegularExpressionMatch match = matchIterator.next(); - setFormat(match.capturedStart(), match.capturedLength(), rule.format); + return; + } + + auto keywords = config["keywords"]; + for (const auto &category : keywords) + { + const std::string key = category.first.as(); + const YAML::Node &rules = category.second; + + // Iterate through each rule in the category + for (const auto &rule : rules) + { + + QString regex; + try + { + std::string regexStr = rule["regex"].as(); //will throw exception if the key does not exist + regex = QString::fromStdString(regexStr); + } + catch(const YAML::Exception e) + { + qWarning() << " YAML exception when parsion the regex in syntax file" << e.what(); + continue; + } + + qDebug() << "regex: " << regex; + + QColor color; + try + { + std::string colorStr = rule["color"].as(); + color = QColor(QString::fromStdString(colorStr)); + } + catch(const YAML::Exception e) + { + qWarning() << " YAML exception when parsion the color in syntax file" << e.what(); + continue; + } + + //checks if the color is a valid color + if(!color.isValid()) + { + qWarning() << "Invalid COlor : Skipping..."; + continue; + } + + // Create a QTextCharFormat for the rule + QTextCharFormat format; + format.setForeground(color); + if (rule["bold"] && rule["bold"].as()) + { + format.setFontWeight(QFont::Bold); + } + if (rule["italic"] && rule["italic"].as()) + { + format.setFontItalic(true); + } + + // Append the rule to the list of syntax rules + m_syntaxRules.append({QRegularExpression(regex), format}); + } } - } } diff --git a/src/SyntaxManager.cpp b/src/SyntaxManager.cpp new file mode 100644 index 0000000..9a8bf8f --- /dev/null +++ b/src/SyntaxManager.cpp @@ -0,0 +1,76 @@ +#include "SyntaxManager.h" +#include "Syntax.h" + +#include +#include +#include + +std::unique_ptr SyntaxManager::createSyntaxHighlighter(const QString &extension, QTextDocument *doc) +{ + QString configPath = qgetenv("CONFIG_DIR"); + if (configPath.isEmpty()) + { + configPath = "config"; + } + + QDir syntaxDir(configPath); + + QStringList yamlFiles = syntaxDir.entryList({"*.yaml", "*.yml"}, QDir::Files); + qDebug() << "Directory being scanned: " << syntaxDir.absolutePath(); + + if (syntaxDir.exists()) + { + qDebug() << "Directory exists."; + } + else + { + qDebug() << "Directory does not exist."; + } + + std::vector config; + // Iterate over all YAML files and store their contents as separate nodes + for (const QString &fileName : yamlFiles) + { + QFile file(syntaxDir.filePath(fileName)); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + YAML::Node fileConfig = YAML::Load(file.readAll().toStdString()); + file.close(); + qDebug() << "Loaded YAML from: " << file.fileName(); + + config.push_back(fileConfig); + } + else + { + qDebug() << "Failed to open file: " << file.fileName(); + } + } + + return createHighlighter(doc, config, extension); +} + +std::unique_ptr SyntaxManager::createHighlighter(QTextDocument *doc, const std::vector &config, const QString &extension) +{ + qDebug() << "Creating highlighter for extension:" << extension; + for (const auto &node : config) + { + if (node["extensions"]) + { + for (const auto &ext : node["extensions"]) + { + std::string extensionInConfig = ext.as(); + if (extensionInConfig == extension.toStdString()) + { + return std::make_unique(doc, node); + } + } + } + else + { + qDebug() << "No extensions key in YAML config."; + } + } + + qDebug() << "No matching highlighter found for extension:" << extension; + return nullptr; +} diff --git a/src/Tree.cpp b/src/Tree.cpp index c5405a0..581685a 100644 --- a/src/Tree.cpp +++ b/src/Tree.cpp @@ -1,78 +1,229 @@ #include "Tree.h" -#include "MainWindow.h" #include "CodeEditor.h" #include #include +#include #include #include +#include +#include +#include +#include -Tree::Tree(QSplitter *splitter, MainWindow *mainWindow) : QObject(splitter), mainWindow(mainWindow) +Tree::Tree(QSplitter *splitter) + : QObject(splitter), + m_iconProvider(std::make_unique()), + m_model(std::make_unique()), + m_tree(std::make_unique(splitter)) { - model = new QFileSystemModel(); - tree = new QTreeView(splitter); - - setupModel(); - setupTree(); - - connect(tree, &QTreeView::doubleClicked, this, &Tree::openFile); + connect(m_tree.get(), &QTreeView::doubleClicked, this, &Tree::openFile); } Tree::~Tree() {} -void Tree::setupModel() +void Tree::initialize(const QString &directory) { - model->setRootPath(getDirectoryPath()); - model->setIconProvider(new QFileIconProvider); - model->setFilter(QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot); + setupModel(directory); + setupTree(); +} +void Tree::setupModel(const QString &directory) +{ + m_model->setRootPath(directory); + m_model->setIconProvider(m_iconProvider.get()); + m_model->setFilter(QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot); } void Tree::setupTree() { - tree->setModel(model); - tree->setRootIndex(model->index(model->rootPath())); - tree->setRootIsDecorated(true); - tree->setAnimated(true); - tree->setIndentation(20); - tree->setSortingEnabled(false); - tree->sortByColumn(1, Qt::AscendingOrder); - - tree->setContextMenuPolicy(Qt::CustomContextMenu); - connect(tree, &QTreeView::customContextMenuRequested, this, &Tree::showContextMenu); - - for (int i = 1; i <= 3; ++i) - { - tree->setColumnHidden(i, true); - } + m_tree->setModel(m_model.get()); + m_tree->setRootIndex(m_model->index(m_model->rootPath())); + m_tree->setRootIsDecorated(true); + m_tree->setAnimated(true); + m_tree->setIndentation(20); + m_tree->setSortingEnabled(false); + m_tree->sortByColumn(1, Qt::AscendingOrder); + m_tree->setHeaderHidden(true); + + for (int i = 1; i <= m_model->columnCount(); ++i) + { + m_tree->setColumnHidden(i, true); + } + + m_tree->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_tree.get(), &QTreeView::customContextMenuRequested, this, &Tree::showContextMenu); } -QString Tree::getDirectoryPath() +void Tree::openFile(const QModelIndex &index) { - return QFileDialog::getExistingDirectory( - nullptr, QObject::tr("Open Directory"), QDir::homePath(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + QString filePath = m_model->filePath(index); + QFileInfo fileInfo(filePath); + + // Ensure it's a file, not a folder before loading + if (!fileInfo.exists() || !fileInfo.isFile()) + { + qWarning() << "Selected index is not a valid file:" << filePath; + return; + } + + FileManager::getInstance().setCurrentFileName(filePath); + FileManager::getInstance().loadFileInEditor(filePath); } -void Tree::openFile(const QModelIndex &index) +QFileSystemModel *Tree::getModel() const { - QString filePath = model->filePath(index); - QFileInfo fileInfo(filePath); - - // Ensure it's a file, not a folder before loading - if (fileInfo.isFile()) - { - mainWindow->loadFileInEditor(filePath); - } + 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 void Tree::showContextMenu(const QPoint &pos) { - // TO_DO: Implement delete a file - // TO_DO: Implement rename a file - // TO_DO: Implement create a new file - // TO_DO: Implement create a new folder + QMenu contextMenu; + + 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) + { + QFileInfo pathInfo = getPathInfo(); + if (!pathInfo.exists()) + { + qWarning() << "Path does not exist: " << pathInfo.fileName(); + return; + } - // use pos param for testing purpose for now - tree->indexAt(pos); + 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) + { + 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) + { + 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) + { + 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/src/main.cpp b/src/main.cpp index b7aa619..93247d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,19 +13,22 @@ QIcon createRoundIcon(const QString &iconPath) { QPixmap pixmap(iconPath); - - // Create a round mask - QBitmap mask(pixmap.size()); - mask.fill(Qt::white); - - QPainter painter(&mask); - painter.setBrush(Qt::black); + if (pixmap.isNull()) + { + qWarning() << "Failed to load icon:" << iconPath; + return QIcon(); + } + + QPixmap roundPixmap(pixmap.size()); + roundPixmap.fill(Qt::transparent); + + QPainter painter(&roundPixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setBrush(QBrush(pixmap)); painter.setPen(Qt::NoPen); painter.drawEllipse(0, 0, pixmap.width(), pixmap.height()); - pixmap.setMask(mask); - - return QIcon(pixmap); + return QIcon(roundPixmap); } int main(int argc, char *argv[]) @@ -33,17 +36,26 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); QIcon roundIcon = createRoundIcon(":/resources/app_icon.png"); - QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + if (roundIcon.isNull()) + { + qWarning() << "Failed to load round icon!"; + } + + QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); font.setPointSize(12); - + app.setFont(font); app.setWindowIcon(roundIcon); - app.setApplicationVersion("0.0.1"); - app.setOrganizationName("Chris Dedman"); - app.setApplicationName("CodeAstra"); - MainWindow window; - window.show(); + app.setApplicationVersion(QStringLiteral("0.1.0")); + app.setOrganizationName(QStringLiteral("Chris Dedman")); + app.setApplicationName(QStringLiteral("CodeAstra")); + app.setApplicationDisplayName(QStringLiteral("CodeAstra")); + + app.setStyle("Fusion"); + + QScopedPointer window(new MainWindow); + window->show(); return app.exec(); } \ No newline at end of file diff --git a/test_with_xvbf.sh b/test_with_xvbf.sh new file mode 100644 index 0000000..c0b8574 --- /dev/null +++ b/test_with_xvbf.sh @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# Start Xvfb server +Xvfb :99 -screen 0 1024x768x24 & +export DISPLAY=:99 + +# Run tests +make test diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..1f00856 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,27 @@ +enable_testing() + +find_package(yaml-cpp REQUIRED CONFIG) + +# 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_mainwindow.cpp b/tests/test_mainwindow.cpp new file mode 100644 index 0000000..14567c0 --- /dev/null +++ b/tests/test_mainwindow.cpp @@ -0,0 +1,106 @@ +#include +#include +#include + +#include "MainWindow.h" +#include "CodeEditor.h" +#include "FileManager.h" + +class TestMainWindow : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + void testWindowTitle(); + void testEditorInitialization(); + void testMenuBar(); + void testInitTree(); + void testCreateAction(); + +private: + std::unique_ptr mainWindow; +}; + +void TestMainWindow::initTestCase() +{ + qDebug() << "Initializing MainWindow tests..."; + mainWindow = std::make_unique(); + mainWindow->show(); +} + +void TestMainWindow::cleanupTestCase() +{ + qDebug() << "Cleaning up MainWindow tests..."; + mainWindow.reset(); +} + +void TestMainWindow::testWindowTitle() +{ + QCOMPARE(mainWindow->windowTitle(), "CodeAstra ~ Code Editor"); +} + +void TestMainWindow::testEditorInitialization() +{ + QVERIFY2(mainWindow->findChild() != nullptr, + "MainWindow must contain a CodeEditor."); +} + +void TestMainWindow::testMenuBar() +{ + QMenuBar *menuBar = mainWindow->menuBar(); + QVERIFY2(menuBar != nullptr, "MainWindow must have a QMenuBar."); + QCOMPARE_EQ(menuBar->actions().size(), 3); // File, Help, CodeAstra + + QMenu *fileMenu = menuBar->findChild("File"); + QVERIFY2(fileMenu != nullptr, "QMenuBar must contain a 'File' menu."); + QCOMPARE_EQ(fileMenu->title(), "File"); + + QMenu *helpMenu = menuBar->findChild("Help"); + QVERIFY2(helpMenu != nullptr, "QMenuBar must contain a 'Help' menu."); + QCOMPARE_EQ(helpMenu->title(), "Help"); + + QMenu *appMenu = menuBar->findChild("CodeAstra"); + QVERIFY2(appMenu != nullptr, "QMenuBar must contain a 'CodeAstra' menu."); + QCOMPARE_EQ(appMenu->title(), "CodeAstra"); +} + +void TestMainWindow::testInitTree() +{ + QSplitter *splitter = dynamic_cast(mainWindow->centralWidget()); + QVERIFY2(splitter != nullptr, "Central widget should be a QSplitter."); + + QCOMPARE_EQ(splitter->handleWidth(), 5); + QCOMPARE_EQ(splitter->childrenCollapsible(), false); + QCOMPARE_EQ(splitter->opaqueResize(), true); + + QList sizes = splitter->sizes(); + QCOMPARE_EQ(sizes.size(), 2); +} + +void TestMainWindow::testCreateAction() +{ + // Mock parameters for createAction + QIcon icon; + QString text = "Test Action"; + QKeySequence shortcut = QKeySequence(Qt::CTRL | Qt::Key_T); + QString statusTip = "This is a test action"; + bool slotCalled = false; + + auto slot = [&slotCalled]() { slotCalled = true; }; + + QAction *action = mainWindow->createAction(icon, text, shortcut, statusTip, slot); + + QVERIFY2(action != nullptr, "Action should be successfully created."); + QCOMPARE_EQ(action->text(), text); + QCOMPARE_EQ(action->shortcuts().first(), shortcut); + QCOMPARE_EQ(action->statusTip(), statusTip); + + // Simulate triggering the action + action->trigger(); + QCOMPARE_EQ(slotCalled, true); +} + +QTEST_MAIN(TestMainWindow) +#include "test_mainwindow.moc" \ No newline at end of file diff --git a/tests/test_syntax.cpp b/tests/test_syntax.cpp new file mode 100644 index 0000000..0166bbe --- /dev/null +++ b/tests/test_syntax.cpp @@ -0,0 +1,105 @@ +#include +#include "Syntax.h" + +#include +#include +#include + +// Helper function to create a YAML node for testing +YAML::Node createTestConfig() +{ + YAML::Node config; + YAML::Node keywords = config["keywords"]; + + YAML::Node category1; + YAML::Node rule1; + rule1["regex"] = "\\bint\\b"; + rule1["color"] = "#ff0000"; + rule1["bold"] = true; + rule1["italic"] = false; + category1.push_back(rule1); + + YAML::Node rule2; + rule2["regex"] = "\\bfloat\\b"; + rule2["color"] = "#00ff00"; + rule2["bold"] = false; + rule2["italic"] = true; + category1.push_back(rule2); + + keywords["types"] = category1; + + return config; +} + +class TestSyntax : public QObject +{ + Q_OBJECT + +protected: + QTextDocument *document; + Syntax *syntax; + +private slots: + void initTestCase(); + void cleanupTestCase(); + void testLoadValidSyntaxRules(); + void testLoadEmptySyntaxRules(); + void testLoadMissingKeywords(); +}; + +void TestSyntax::initTestCase() +{ + qDebug() << "Initializing TestSyntax tests..."; + document = new QTextDocument(); + syntax = new Syntax(document, YAML::Node()); +} + +void TestSyntax::cleanupTestCase() +{ + qDebug() << "Cleaning up TestSyntax tests..."; +} + +void TestSyntax::testLoadEmptySyntaxRules() +{ + YAML::Node config; + syntax->loadSyntaxRules(config); + + // Verify that no rules were loaded + QVERIFY(syntax->m_syntaxRules.isEmpty()); +} + +void TestSyntax::testLoadValidSyntaxRules() +{ + YAML::Node config = createTestConfig(); + syntax->loadSyntaxRules(config); + + // Verify that the rules were loaded correctly + QVERIFY(syntax->m_syntaxRules.size() == 2); + + // Check the first rule + const auto &rule1 = syntax->m_syntaxRules[0]; + QCOMPARE_EQ(rule1.m_pattern.pattern(), "\\bint\\b"); + QCOMPARE_EQ(rule1.m_format.foreground().color(), QColor("#ff0000")); + QCOMPARE_EQ(rule1.m_format.fontWeight(), QFont::Bold); + QCOMPARE_NE(rule1.m_format.fontItalic(), true); + + // Check the second rule + const auto &rule2 = syntax->m_syntaxRules[1]; + QCOMPARE_EQ(rule2.m_pattern.pattern(), "\\bfloat\\b"); + QCOMPARE_EQ(rule2.m_format.foreground().color(), QColor("#00ff00")); + QCOMPARE_EQ(rule2.m_format.fontWeight(), QFont::Normal); + QCOMPARE_EQ(rule2.m_format.fontItalic(), true); +} + +void TestSyntax::testLoadMissingKeywords() +{ + YAML::Node config; + config["other_section"] = YAML::Node(); + syntax->loadSyntaxRules(config); + + // Verify that no rules were loaded + QVERIFY(syntax->m_syntaxRules.isEmpty()); +} + +QTEST_MAIN(TestSyntax) +#include "test_syntax.moc" \ No newline at end of file