From 821ea7eed0f5de6ee50d2e820c307cc23d337cdb Mon Sep 17 00:00:00 2001 From: Timofey Khoruzhii Date: Sun, 16 Apr 2023 00:23:12 +0300 Subject: [PATCH] init project --- .gitignore | 2 + CMakeLists.txt | 46 +++++ include/lua/loader.hpp | 28 ++++ include/projects/project.hpp | 32 ++++ include/projects/project_list.hpp | 24 +++ include/utils/command_parser.hpp | 54 ++++++ include/utils/generate_project_directory.hpp | 6 + include/utils/local.hpp | 17 ++ src/lua/loader.cpp | 154 +++++++++++++++++ src/main.cpp | 142 ++++++++++++++++ src/projects/project.cpp | 59 +++++++ src/projects/project_list.cpp | 78 +++++++++ src/utils/command_parser.cpp | 167 +++++++++++++++++++ src/utils/gererate_project_directory.cpp | 29 ++++ tests/utils/command_parser.cpp | 104 ++++++++++++ zsh_autocomplete/_cl | 8 + 16 files changed, 950 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 include/lua/loader.hpp create mode 100644 include/projects/project.hpp create mode 100644 include/projects/project_list.hpp create mode 100644 include/utils/command_parser.hpp create mode 100644 include/utils/generate_project_directory.hpp create mode 100644 include/utils/local.hpp create mode 100644 src/lua/loader.cpp create mode 100644 src/main.cpp create mode 100644 src/projects/project.cpp create mode 100644 src/projects/project_list.cpp create mode 100644 src/utils/command_parser.cpp create mode 100644 src/utils/gererate_project_directory.cpp create mode 100644 tests/utils/command_parser.cpp create mode 100755 zsh_autocomplete/_cl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4fb4fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f8137b5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +# Set the minimum required version of CMake +cmake_minimum_required(VERSION 3.10) + +project(Cl3 VERSION 1.0) + +# Set the C++ standard to C++20 +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +# Enable "compile_commands.json" output for use with various tools +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Add the Catch2 library +find_package(Catch2 REQUIRED) +find_package(yaml-cpp REQUIRED) +find_package(Lua 5.1 REQUIRED) + +# Add all source files to a single target +file(GLOB_RECURSE SOURCES "src/*.cpp") +list(FILTER SOURCES EXCLUDE REGEX ".*main.cpp$") +add_executable(cl ${SOURCES} src/main.cpp) +target_include_directories(cl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${LUA_INCLUDE_DIR}) +target_link_libraries(cl PRIVATE yaml-cpp tmuxub ${LUA_LIBRARIES}) +target_compile_options(cl PRIVATE -g) + +# Add all test files to a single target +file(GLOB_RECURSE TEST_SOURCES "tests/*.cpp") +add_executable(test_cl ${TEST_SOURCES} ${SOURCES}) +target_link_libraries(test_cl PRIVATE Catch2::Catch2WithMain yaml-cpp ${LUA_LIBRARIES}) +target_include_directories(test_cl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CATCH_INCLUDE_DIRS} ${LUA_INCLUDE_DIR}) + +# Enable testing +enable_testing() + +# Add the tests to the test suite +include(Catch) +catch_discover_tests(test_cl) +add_test(NAME TestCl COMMAND test_cl) + +include(FetchContent) +FetchContent_Declare( + tmuxub + GIT_REPOSITORY https://gitlab.com/Onyad/tmuxub + GIT_TAG origin/main +) +FetchContent_MakeAvailable(tmuxub) diff --git a/include/lua/loader.hpp b/include/lua/loader.hpp new file mode 100644 index 0000000..8836efa --- /dev/null +++ b/include/lua/loader.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include + +class LuaLoader { + public: + LuaLoader(); + + ~LuaLoader(); + + void AddLuaPath(const std::string&); + void AddCPath(const std::string&); + void Call(const std::string& lua_module, const std::string& function); + void LoadToGlobal(const std::string&); + + std::vector GetListScripts(const std::string&) const; + + private: + void Import(const std::string&, const std::string&, int); + void LoadImpl(const std::string&); + + void ForEachByStringArray(std::function); + void ParseRequire(); + void ParseImport(); + + lua_State* L; +}; diff --git a/include/projects/project.hpp b/include/projects/project.hpp new file mode 100644 index 0000000..6c1c763 --- /dev/null +++ b/include/projects/project.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +class Project { + public: + Project(const std::string& path, const std::string& path_to_scripts, const std::string& name); + Project(const Project&) = delete; + Project(Project&&) = default; + + const std::filesystem::path& GetPath() const; + const std::filesystem::path& GetPathToScripts() const; + const std::string& GetName() const; + bool IsSubDirectory(const std::filesystem::path&) const; + + void SetPath(const std::string& path); + void SetPathToScripts(const std::string& path_to_scripts); + void SetName(const std::string& name); + + void PrintInfo() const; + + private: + std::filesystem::path path_; + std::filesystem::path path_to_scripts_; + std::string name_; +}; + +Project CreateProject(); +Project CreateProject(const std::string& name); diff --git a/include/projects/project_list.hpp b/include/projects/project_list.hpp new file mode 100644 index 0000000..dac6f4e --- /dev/null +++ b/include/projects/project_list.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "projects/project.hpp" + +class ProjectList { + public: + ProjectList(); + + const std::deque& GetProjects(); + void AddProject(Project&&); + + const Project& GetProject( + const std::filesystem::path& path = std::filesystem::current_path()) const; + void LoadFromYaml(const std::string& file_path); + void SaveToYaml(const std::string& file_path) const; + + private: + std::deque projects_; +}; diff --git a/include/utils/command_parser.hpp b/include/utils/command_parser.hpp new file mode 100644 index 0000000..2129958 --- /dev/null +++ b/include/utils/command_parser.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +enum class PatternType { FixedWord, AnyWord, AnyWords }; + +struct Pattern { + PatternType type; + std::string fixed_word; +}; + +using Callback = std::variant&)>, + std::function&)>>; + +class Parser { + public: + void AddRule(const std::vector& pattern, const auto& callback, + bool find_in_suggestion = true) { + if constexpr (std::is_same_v< + decltype(callback(std::declval&>())), void>) { + AddRuleImpl(pattern, std::function&)>(callback), + find_in_suggestion); + } else { + AddRuleImpl(pattern, std::function&)>(callback), + find_in_suggestion); + } + } + void Parse(int argc, char* argv[]) const; + std::vector GetPossibleAdditions(int argc, char* argv[]) const; + std::vector GetPossibleAdditions(const std::vector&) const; + + private: + void AddRuleImpl(const std::vector& pattern, const Callback& callback, + bool find_in_suggestion); + bool Match(const std::vector& pattern, const std::vector& args) const; + + private: + struct Rule { + Rule(const std::vector& pattern, Callback callback, bool find_in_suggestion) + : pattern(pattern), callback(callback), find_in_suggestion(find_in_suggestion) {} + std::vector pattern; + Callback callback; + bool find_in_suggestion; + }; + + std::vector rules_; +}; + +std::vector MakePattern( + std::initializer_list> elements); diff --git a/include/utils/generate_project_directory.hpp b/include/utils/generate_project_directory.hpp new file mode 100644 index 0000000..d0c25f5 --- /dev/null +++ b/include/utils/generate_project_directory.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +std::string generate_project_directory(const std::string& project_name); diff --git a/include/utils/local.hpp b/include/utils/local.hpp new file mode 100644 index 0000000..6cf2149 --- /dev/null +++ b/include/utils/local.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +inline std::filesystem::path GetLocalPath() { + const char* home_dir = getenv("HOME"); + if (home_dir == nullptr) { + std::cerr << "Home directory was not declared" << std::endl; + throw std::runtime_error("Was not found home directory"); + } + std::filesystem::path result = std::filesystem::path(home_dir) / ".local/share/cl3"; + std::filesystem::create_directories(result); + + return result; +} diff --git a/src/lua/loader.cpp b/src/lua/loader.cpp new file mode 100644 index 0000000..45089e2 --- /dev/null +++ b/src/lua/loader.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include + +LuaLoader::LuaLoader() : L(luaL_newstate()) { + luaL_openlibs(L); + AddLuaPath("./?.lua"); + AddCPath("./?.so"); + AddCPath("./?.dylib"); +} + +void LuaLoader::AddLuaPath(const std::string& new_path) { + lua_getglobal(L, "package"); + lua_getfield(L, -1, "path"); + lua_pushstring(L, (std::string(";") + new_path).data()); + lua_concat(L, 2); + lua_setfield(L, -2, "path"); + lua_pop(L, 1); +} + +void LuaLoader::AddCPath(const std::string& new_path) { + lua_getglobal(L, "package"); + lua_getfield(L, -1, "cpath"); + lua_pushstring(L, (std::string(";") + new_path).data()); + lua_concat(L, 2); + lua_setfield(L, -2, "cpath"); + lua_pop(L, 1); +} + +void LuaLoader::LoadImpl(const std::string& name) { + lua_getglobal(L, "require"); + lua_pushstring(L, name.data()); + + if (lua_pcall(L, 1, 1, 0) != LUA_OK) { + std::cerr << "Error call script: " << name.data() << std::endl; + std::cerr << lua_tostring(L, -1) << std::endl; + throw std::logic_error("Error call script: " + name); + } + + ParseRequire(); + ParseImport(); +} + +void LuaLoader::LoadToGlobal(const std::string& name) { + LoadImpl(name); + lua_setglobal(L, name.data()); +} + +std::vector LuaLoader::GetListScripts(const std::string& lua_module) const { + std::vector result; + + lua_getglobal(L, lua_module.data()); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + continue; + } + + lua_pop(L, 1); + result.push_back(lua_tostring(L, -1)); + } + + lua_pop(L, 1); + + return result; +} + +void LuaLoader::ForEachByStringArray(std::function callback) { + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + + if (lua_isstring(L, -1)) { + callback(lua_tostring(L, -1)); + + lua_pop(L, 1); + return; + } else if (lua_istable(L, -1)) { + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (!lua_isstring(L, -1)) { + throw std::logic_error("Error in for each by string array"); + } + + callback(lua_tostring(L, -1)); + lua_pop(L, 1); + } + + lua_pop(L, 1); + return; + } + + throw std::logic_error("Error in parse requires "); +} + +void LuaLoader::ParseRequire() { + if (!lua_istable(L, -1)) { + return; + } + + lua_getfield(L, -1, "require"); + + ForEachByStringArray( + [this](const std::string& new_module_name) { LoadToGlobal(new_module_name); }); +} + +void LuaLoader::Import(const std::string& name, const std::string& field, int table) { + LoadImpl(name); + lua_getfield(L, -1, field.data()); + lua_setfield(L, table, field.data()); + lua_pop(L, 1); +} + +void LuaLoader::ParseImport() { + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + lua_getfield(L, -1, "import"); + + ForEachByStringArray([this, table = lua_absindex(L, -2)](const std::string& new_module_name) { + int dot_position = new_module_name.find("."); + + if (dot_position == std::string::npos) { + throw std::logic_error("Can't find dot in import"); + } + + Import(new_module_name.substr(0, dot_position), new_module_name.substr(dot_position + 1), + table); + }); +} + +void LuaLoader::Call(const std::string& lua_module, const std::string& function) { + lua_getglobal(L, lua_module.data()); + lua_getfield(L, -1, function.data()); + + if (lua_pcall(L, 0, 0, 0) != LUA_OK) { + std::cout << "Error in call " << lua_module << "." << function << std::endl; + std::cerr << lua_tostring(L, -1) << std::endl; + throw std::logic_error("Error in call " + lua_module + "." + function); + } + + lua_pop(L, 1); +} + +LuaLoader::~LuaLoader() { + lua_close(L); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..736f16b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,142 @@ +#include +#include +#include "utils/command_parser.hpp" +#include "projects/project_list.hpp" +#include "utils/local.hpp" +#include "lua/loader.hpp" + +#include + +namespace fs = std::filesystem; + +void OpenFileInEditor(const std::filesystem::path& filename) { + const char* editor = getenv("EDITOR"); + + if (!editor) { + editor = "vim"; + } + + auto str = filename.string(); + + std::vector argv = {editor, str.data(), nullptr}; + execvp(editor, const_cast(argv.data())); +} + +int main(int argc, char* argv[]) { + ProjectList projects; + projects.LoadFromYaml(GetLocalPath() / "projects.yml"); + + LuaLoader lua; + lua.AddLuaPath((GetLocalPath() / "globals/") / "?.lua"); + lua.AddCPath((GetLocalPath() / "globals/") / "?.so"); + + Parser parser; + + try { + const auto& project = projects.GetProject(); + lua.AddLuaPath(project.GetPathToScripts() / "?.lua"); + + std::vector scripts; + for (auto entry : fs::directory_iterator(project.GetPathToScripts())) { + auto filename = entry.path().filename().string(); + if (filename.ends_with(".lua")) { + scripts.push_back(filename.substr(0, filename.size() - 4)); + } + } + + for (auto script_name : scripts) { + lua.LoadToGlobal(script_name); + + if (script_name == "main") { + auto all_scripts = lua.GetListScripts("main"); + + for (auto& script : all_scripts) { + parser.AddRule(MakePattern({script.data()}), [script, &lua, &project](auto& args) { + fs::current_path(project.GetPath()); + lua.Call("main", script); + }); + } + } else { + auto all_scripts = lua.GetListScripts(script_name); + + for (auto& script : all_scripts) { + parser.AddRule(MakePattern({script_name.string().data(), script.data()}), + [script, &lua, &project, script_name](auto& args) { + std::filesystem::current_path(project.GetPath()); + lua.Call(script_name, script); + }); + } + } + } + + if (std::find(scripts.begin(), scripts.end(), "main") != scripts.end()) { + } + } catch (...) { + } + + for (auto& project : projects.GetProjects()) { + parser.AddRule(MakePattern({".", "open", project.GetName().data()}), [&project](auto& args) { + tmuxub::Tmux t(GetLocalPath() / "tmuxsocket"); + + if (t.HasSession(project.GetName())) { + t.ConnectToSession(project.GetName()); + t.Exec(); + } else { + fs::current_path(project.GetPath()); + t.CreateSession(project.GetName()); + t.Exec(); + } + + std::cout << "try open: " << project.GetName() << std::endl; + }); + } + + parser.AddRule(MakePattern({"help"}), [](auto& args) { std::cout << "It's help" << std::endl; }); + + parser.AddRule(MakePattern({".", "new"}), + [&projects](auto& args) { projects.AddProject(CreateProject()); }); + + parser.AddRule(MakePattern({".", "new", PatternType::AnyWord}), + [&projects](auto& args) { projects.AddProject(CreateProject(args[3])); }); + + parser.AddRule(MakePattern({"."}), [&projects](auto& args) { + const auto& project = projects.GetProject(); + OpenFileInEditor((project.GetPathToScripts() / "main.lua").string()); + }); + + parser.AddRule(MakePattern({".", "global", PatternType::AnyWord}), [&projects](auto& args) { + OpenFileInEditor(GetLocalPath() / "globals" / (args[3] + ".lua")); + }); + + parser.AddRule(MakePattern({".", "config"}), [&projects](auto& args) { + const auto& project = projects.GetProject(); + OpenFileInEditor((project.GetPathToScripts() / "main.lua").string()); + }); + + parser.AddRule(MakePattern({".", "config", PatternType::AnyWord}), [&projects](auto& args) { + const auto& project = projects.GetProject(); + OpenFileInEditor(project.GetPathToScripts() / (args[3] + ".lua")); + }); + + parser.AddRule(MakePattern({".", "projects"}), + [](auto& args) { OpenFileInEditor(GetLocalPath() / "projects.yml"); }); + + parser.AddRule(MakePattern({PatternType::AnyWords}), + [](auto& args) { std::cout << "Unknown command" << std::endl; }); + + if (getenv("CL_SUGGESTION") != nullptr) { + auto suggestions = parser.GetPossibleAdditions(argc, argv); + int x = 0; + for (auto& suggestion : suggestions) { + x++; + if (x == 7) { + break; + } + std::cout << suggestion << " "; + } + } else { + parser.Parse(argc, argv); + + projects.SaveToYaml(GetLocalPath() / "projects.yml"); + } +} diff --git a/src/projects/project.cpp b/src/projects/project.cpp new file mode 100644 index 0000000..13097d2 --- /dev/null +++ b/src/projects/project.cpp @@ -0,0 +1,59 @@ +#include "projects/project.hpp" +#include "utils/local.hpp" + +#include + +namespace fs = std::filesystem; + +Project::Project(const std::string& path, const std::string& path_to_scripts, + const std::string& name) + : path_(path), path_to_scripts_(path_to_scripts), name_(name) {} + +const fs::path& Project::GetPath() const { + return path_; +} + +const fs::path& Project::GetPathToScripts() const { + return path_to_scripts_; +} + +const std::string& Project::GetName() const { + return name_; +} + +bool Project::IsSubDirectory(const fs::path& path) const { + return path.string().starts_with(path_.string()); +} + +void Project::SetPath(const std::string& path) { + path_ = path; +} + +void Project::SetPathToScripts(const std::string& path_to_scripts) { + path_to_scripts_ = path_to_scripts; +} + +void Project::SetName(const std::string& name) { + name_ = name; +} + +void Project::PrintInfo() const { + std::cout << "Project Name: " << name_ << "\n"; + std::cout << "Path: " << path_ << "\n"; + std::cout << "Path to Scripts: " << path_to_scripts_ << "\n"; +} + +Project CreateProject() { + fs::path current_path = fs::current_path(); + return CreateProject(current_path.filename().string()); +} + +Project CreateProject(const std::string& name) { + fs::path current_path = fs::current_path(); + std::string path = current_path.string(); + + std::string path_to_scripts = GetLocalPath() / "projects" / name; + fs::create_directories(path_to_scripts); + + return Project(path, path_to_scripts, name); +} diff --git a/src/projects/project_list.cpp b/src/projects/project_list.cpp new file mode 100644 index 0000000..d13e5b5 --- /dev/null +++ b/src/projects/project_list.cpp @@ -0,0 +1,78 @@ +#include + +#include "projects/project_list.hpp" + +ProjectList::ProjectList() {} + +const std::deque& ProjectList::GetProjects() { + return projects_; +} + +void ProjectList::AddProject(Project&& project) { + bool exists = + std::find_if(projects_.begin(), projects_.end(), [&project](const Project& another) { + return another.GetName() == project.GetName(); + }) != projects_.end(); + + if (exists) { + throw std::logic_error("Project with same name exists"); + } + + projects_.push_back(std::move(project)); +} + +const Project& ProjectList::GetProject(const std::filesystem::path& path) const { + size_t index = 0, prefix = 0; + for (size_t i = 0; i < projects_.size(); ++i) { + const auto& project = projects_[i]; + if (project.IsSubDirectory(path)) { + if (prefix < project.GetPath().string().size()) { + index = i; + prefix = project.GetPath().string().size(); + } + } + } + + if (prefix == 0) { + throw std::logic_error("Project not exists"); + } + + return projects_[index]; +} + +void ProjectList::LoadFromYaml(const std::string& file_path) { + try { + YAML::Node yaml = YAML::LoadFile(file_path); + for (const auto& project : yaml["projects"]) { + std::string path = project["path"].as(); + std::string path_to_scripts = project["path_to_scripts"].as(); + std::string name = project["name"].as(); + projects_.emplace_back(path, path_to_scripts, name); + } + } catch (YAML::Exception& e) { + std::cerr << "Error loading YAML file: " << e.what() << "\n"; + } +} + +void ProjectList::SaveToYaml(const std::string& file_path) const { + YAML::Emitter yaml; + yaml << YAML::BeginMap; + yaml << YAML::Key << "projects"; + yaml << YAML::Value << YAML::BeginSeq; + for (const auto& project : projects_) { + yaml << YAML::BeginMap; + yaml << YAML::Key << "path"; + yaml << YAML::Value << project.GetPath(); + yaml << YAML::Key << "path_to_scripts"; + yaml << YAML::Value << project.GetPathToScripts(); + yaml << YAML::Key << "name"; + yaml << YAML::Value << project.GetName(); + yaml << YAML::EndMap; + } + yaml << YAML::EndSeq; + yaml << YAML::EndMap; + + std::ofstream output(file_path); + output << yaml.c_str(); + output.close(); +} diff --git a/src/utils/command_parser.cpp b/src/utils/command_parser.cpp new file mode 100644 index 0000000..d7a35e3 --- /dev/null +++ b/src/utils/command_parser.cpp @@ -0,0 +1,167 @@ +#include "utils/command_parser.hpp" +#include + +void Parser::AddRuleImpl(const std::vector& pattern, const Callback& callback, + bool find_in_suggestion) { + rules_.emplace_back(pattern, callback, find_in_suggestion); +} + +void Parser::Parse(int argc, char* argv[]) const { + std::vector args(argv, argv + argc); + + for (const auto& [pattern, callback, _] : rules_) { + if (Match(pattern, args)) { + if (callback.index() == 0) { + std::get<0>(callback)(args); + return; + } else { + if (std::get<1>(callback)(args)) { + return; + } + } + } + } +} + +std::vector Parser::GetPossibleAdditions(int argc, char* argv[]) const { + if (argc != 2) { + throw std::logic_error("argc should be equal 2"); + } + + size_t last = 0; + std::string s = argv[1]; + + std::vector input; + + for (size_t i = 0; i < s.length(); ++i) { + if (s[i] == ' ') { + input.push_back(s.substr(last, i - last)); + last = i + 1; + } else if (i + 1 == s.length()) { + input.push_back(s.substr(last, i - last + 1)); + } + } + + std::erase_if(input, [](auto x) { return x.empty(); }); + + if (s.back() == ' ') { + input.emplace_back(); + } + + return GetPossibleAdditions(input); +}; + +std::vector Parser::GetPossibleAdditions(const std::vector& input) const { + std::vector suggestions; + + for (const auto& [pattern, callback, check] : rules_) { + if (!check) { + continue; + } + size_t arg_index = 1; + size_t pattern_index = 0; + bool continue_outer_loop = false; + + while (arg_index <= input.size() && pattern_index < pattern.size()) { + switch (pattern[pattern_index].type) { + case PatternType::FixedWord: + if (arg_index == input.size()) { + suggestions.push_back(pattern[pattern_index].fixed_word); + continue_outer_loop = true; + }else if (arg_index + 1 == input.size() && + pattern[pattern_index].fixed_word.starts_with(input[arg_index])) { + suggestions.push_back(pattern[pattern_index].fixed_word); + continue_outer_loop = true; + } else if (input[arg_index] == pattern[pattern_index].fixed_word) { + ++arg_index; + ++pattern_index; + } else { + continue_outer_loop = true; + } + break; + case PatternType::AnyWord: + ++arg_index; + ++pattern_index; + break; + case PatternType::AnyWords: + ++pattern_index; + + if (pattern_index == pattern.size()) { + arg_index = input.size(); + } else { + while (arg_index <= input.size() && + (arg_index == input.size() || + input[arg_index] != pattern[pattern_index].fixed_word)) { + if (arg_index == input.size()) { + suggestions.push_back(pattern[pattern_index].fixed_word); + } + ++arg_index; + } + } + break; + } + if (continue_outer_loop) { + break; + } + } + } + + // std::sort(suggestions.begin(), suggestions.end()); + // suggestions.erase(std::unique(suggestions.begin(), suggestions.end()), suggestions.end()); + return suggestions; +} + +bool Parser::Match(const std::vector& pattern, + const std::vector& args) const { + size_t arg_index = 1; + size_t pattern_index = 0; + + while (arg_index < args.size() && pattern_index < pattern.size()) { + switch (pattern[pattern_index].type) { + case PatternType::FixedWord: + if (args[arg_index] == pattern[pattern_index].fixed_word) { + ++arg_index; + ++pattern_index; + } else { + return false; + } + + break; + case PatternType::AnyWord: + ++arg_index; + ++pattern_index; + break; + case PatternType::AnyWords: + ++pattern_index; + if (pattern_index == pattern.size()) { + arg_index = args.size(); + } else { + while (arg_index < args.size() && args[arg_index] != pattern[pattern_index].fixed_word) { + ++arg_index; + } + } + break; + } + } + + if (pattern_index < pattern.size() && pattern[pattern_index].type == PatternType::AnyWords) { + pattern_index++; + } + + return arg_index == args.size() && pattern_index == pattern.size(); +} + +std::vector MakePattern( + std::initializer_list> elements) { + std::vector pattern; + + for (const auto& element : elements) { + if (std::holds_alternative(element)) { + pattern.push_back({PatternType::FixedWord, std::get(element)}); + } else { + pattern.push_back({std::get(element), ""}); + } + } + + return pattern; +} diff --git a/src/utils/gererate_project_directory.cpp b/src/utils/gererate_project_directory.cpp new file mode 100644 index 0000000..e52b11c --- /dev/null +++ b/src/utils/gererate_project_directory.cpp @@ -0,0 +1,29 @@ +#include "utils/generate_project_directory.hpp" +#include + +namespace fs = std::filesystem; + +std::string generate_project_directory(const std::string& project_name) { + const char* home_dir = std::getenv("HOME"); + if (!home_dir) { + throw std::runtime_error("Could not get HOME environment variable"); + } + fs::path base_dir(home_dir); + + fs::path app_subdir(".local/share/cl3/projets"); + fs::path app_dir = base_dir / app_subdir; + + if (!fs::exists(app_dir)) { + fs::create_directory(app_dir); + } + + fs::path new_dir_path = app_dir / project_name; + + if (fs::exists(new_dir_path)) { + throw std::runtime_error("Project exists"); + } + + fs::create_directory(new_dir_path); + + return new_dir_path.string(); +} diff --git a/tests/utils/command_parser.cpp b/tests/utils/command_parser.cpp new file mode 100644 index 0000000..392f0e6 --- /dev/null +++ b/tests/utils/command_parser.cpp @@ -0,0 +1,104 @@ +#include +#include "utils/command_parser.hpp" + +TEST_CASE("Parser tests", "[parser]") { + SECTION("Fixed word pattern") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command"}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv1[] = {"./program", "command"}; + parser.Parse(2, const_cast(argv1)); + REQUIRE(callbackCalled); + } + + SECTION("Any word pattern") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command", PatternType::AnyWord}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv2[] = {"./program", "command", "any_word"}; + parser.Parse(3, const_cast(argv2)); + REQUIRE(callbackCalled); + } + + SECTION("Any words pattern") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command", PatternType::AnyWords}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv3[] = {"./program", "command", "word1", "word2", "word3"}; + parser.Parse(5, const_cast(argv3)); + REQUIRE(callbackCalled); + } + + SECTION("Mixed pattern") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command", PatternType::AnyWord, "option", PatternType::AnyWords}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv4[] = {"./program", "command", "any_word", "option", "word1", "word2", "word3"}; + parser.Parse(7, const_cast(argv4)); + REQUIRE(callbackCalled); + } + + SECTION("Non-matching pattern") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command", "option", PatternType::AnyWords}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv5[] = {"./program", "other_command", "option", "word1", "word2", "word3"}; + parser.Parse(6, const_cast(argv5)); + REQUIRE(!callbackCalled); + } + + SECTION("Multiple rules with different matches") { + Parser parser; + bool callbackCalled1 = false; + bool callbackCalled2 = false; + + parser.AddRule( + MakePattern({"command1", PatternType::AnyWords}), + [&callbackCalled1](const std::vector& args) { callbackCalled1 = true; }); + + parser.AddRule( + MakePattern({"command2", PatternType::AnyWords}), + [&callbackCalled2](const std::vector& args) { callbackCalled2 = true; }); + + const char* argv6[] = {"./program", "command1", "word1", "word2", "word3"}; + parser.Parse(5, const_cast(argv6)); + REQUIRE(callbackCalled1); + REQUIRE(!callbackCalled2); + const char* argv7[] = {"./program", "command2", "word1", "word2", "word3"}; + parser.Parse(5, const_cast(argv7)); + REQUIRE(callbackCalled2); + } + + SECTION("No matching rule") { + Parser parser; + bool callbackCalled = false; + + parser.AddRule( + MakePattern({"command", "option", PatternType::AnyWords}), + [&callbackCalled](const std::vector& args) { callbackCalled = true; }); + + const char* argv8[] = {"./program", "other_command", "other_option", "word1", "word2", "word3"}; + parser.Parse(6, const_cast(argv8)); + REQUIRE(!callbackCalled); + } +} diff --git a/zsh_autocomplete/_cl b/zsh_autocomplete/_cl new file mode 100755 index 0000000..2dd86d1 --- /dev/null +++ b/zsh_autocomplete/_cl @@ -0,0 +1,8 @@ +_cl() { + local comp=`CL_SUGGESTION="" ${words[1]} "$words"` + + read -A line <<< $comp + compadd -o nosort -- $line +} + +compdef _cl cl