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 @@
+
+
+
+ [](https://github.com/sandbox-science/CodeAstra/actions/workflows/cpp.yml)
+ [](https://github.com/sandbox-science/CodeAstra/actions/workflows/test.yml)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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