diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 7ce3a41e5..06d1ec91b 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -46,11 +46,12 @@ add_library(common util/json_util.cpp util/read_iso_file.cpp util/SimpleThreadGroup.cpp + util/StringUtil.cpp util/Timer.cpp util/os.cpp util/print_float.cpp util/FrameLimiter.cpp - util/unicode_util.cpp ) + util/unicode_util.cpp) target_link_libraries(common fmt lzokay replxx libzstd_static) diff --git a/common/goos/Reader.cpp b/common/goos/Reader.cpp index 139b1ef8b..ce86dd29b 100644 --- a/common/goos/Reader.cpp +++ b/common/goos/Reader.cpp @@ -239,17 +239,9 @@ Object Reader::read_from_string(const std::string& str, * Read a file */ Object Reader::read_from_file(const std::vector& file_path, bool check_encoding) { - std::string joined_name; + std::string joined_path = fmt::format("{}", fmt::join(file_path, "/")); - for (const auto& thing : file_path) { - if (!joined_name.empty()) { - joined_name += '/'; - } - - joined_name += thing; - } - - auto textFrag = std::make_shared(file_util::get_file_path(file_path), joined_name); + auto textFrag = std::make_shared(file_util::get_file_path(file_path), joined_path); db.insert(textFrag); auto result = internal_read(textFrag, check_encoding); diff --git a/common/util/FileUtil.cpp b/common/util/FileUtil.cpp index 6e35aaeb4..eb1dabbfb 100644 --- a/common/util/FileUtil.cpp +++ b/common/util/FileUtil.cpp @@ -321,10 +321,23 @@ std::string base_name(const std::string& filename) { break; } } - return filename.substr(pos); } +std::string base_name_no_ext(const std::string& filename) { + size_t pos = 0; + ASSERT(!filename.empty()); + for (size_t i = filename.size() - 1; i-- > 0;) { + if (filename.at(i) == '/' || filename.at(i) == '\\') { + pos = (i + 1); + break; + } + } + std::string file_name = filename.substr(pos); + return file_name.substr(0, file_name.find_last_of('.')); + ; +} + void ISONameFromAnimationName(char* dst, const char* src) { // The Animation Name is a bunch of words separated by dashes @@ -529,7 +542,7 @@ std::vector find_files_recursively(const fs::path& base_dir, const std std::vector files = {}; for (auto& p : fs::recursive_directory_iterator(base_dir)) { if (p.is_regular_file()) { - if (std::regex_match(fs::path(p.path()).filename().string(), pattern)) { + if (std::regex_match(p.path().filename().string(), pattern)) { files.push_back(p.path()); } } diff --git a/common/util/FileUtil.h b/common/util/FileUtil.h index c40831ef6..10c6869f4 100644 --- a/common/util/FileUtil.h +++ b/common/util/FileUtil.h @@ -52,6 +52,7 @@ bool is_printable_char(char c); std::string combine_path(const std::string& parent, const std::string& child); bool file_exists(const std::string& path); std::string base_name(const std::string& filename); +std::string base_name_no_ext(const std::string& filename); void MakeISOName(char* dst, const char* src); void ISONameFromAnimationName(char* dst, const char* src); void assert_file_exists(const char* path, const char* error_message); diff --git a/common/util/StringUtil.cpp b/common/util/StringUtil.cpp new file mode 100644 index 000000000..467dd68b9 --- /dev/null +++ b/common/util/StringUtil.cpp @@ -0,0 +1,24 @@ +#include "StringUtil.h" + +namespace str_util { + +const std::string WHITESPACE = " \n\r\t\f\v"; + +bool starts_with(const std::string& s, const std::string& prefix) { + return s.rfind(prefix) == 0; +} + +std::string ltrim(const std::string& s) { + size_t start = s.find_first_not_of(WHITESPACE); + return (start == std::string::npos) ? "" : s.substr(start); +} + +std::string rtrim(const std::string& s) { + size_t end = s.find_last_not_of(WHITESPACE); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +std::string trim(const std::string& s) { + return rtrim(ltrim(s)); +} +} // namespace str_util diff --git a/common/util/StringUtil.h b/common/util/StringUtil.h new file mode 100644 index 000000000..04407e84c --- /dev/null +++ b/common/util/StringUtil.h @@ -0,0 +1,8 @@ +#include + +namespace str_util { +bool starts_with(const std::string& s, const std::string& prefix); +std::string ltrim(const std::string& s); +std::string rtrim(const std::string& s); +std::string trim(const std::string& s); +} // namespace str_util diff --git a/goalc/compiler/Compiler.cpp b/goalc/compiler/Compiler.cpp index e9364cd68..46088fc99 100644 --- a/goalc/compiler/Compiler.cpp +++ b/goalc/compiler/Compiler.cpp @@ -67,8 +67,8 @@ Compiler::Compiler(GameVersion version, auto regex_colors = m_repl->regex_colors; m_repl->init_default_settings(); using namespace std::placeholders; - m_repl->get_repl().set_completion_callback( - std::bind(&Compiler::find_symbols_by_prefix, this, _1, _2, std::cref(examples))); + m_repl->get_repl().set_completion_callback(std::bind( + &Compiler::find_symbols_or_object_file_by_prefix, this, _1, _2, std::cref(examples))); m_repl->get_repl().set_hint_callback( std::bind(&Compiler::find_hints_by_prefix, this, _1, _2, _3, std::cref(examples))); m_repl->get_repl().set_highlighter_callback( @@ -382,14 +382,57 @@ void Compiler::setup_goos_forms() { } void Compiler::asm_file(const CompilationOptions& options) { - auto code = m_goos.reader.read_from_file({options.filename}); + // If the filename provided is not a valid path but it's a name (with or without an extension) + // attempt to find it in the defined `asmFileSearchDirs` + // + // For example - (ml "process-drawable.gc") + // - This allows you to load a file without precisely defining the entire path + // + // If multiple candidates are found, abort - std::string obj_file_name = options.filename; + std::string file_name = options.filename; + std::string file_path = file_util::get_file_path({file_name}); + + if (!file_util::file_exists(file_path)) { + if (file_path.empty()) { + lg::print("ERROR - can't load a file without a providing a path\n"); + return; + } else if (m_asm_file_search_dirs.empty()) { + lg::print( + "ERROR - can't load a file that doesn't exist - '{}' and no search dirs are defined\n", + file_path); + return; + } + std::string base_name = file_util::base_name_no_ext(file_path); + // Attempt the find the full path of the file (ignore extension) + std::vector candidate_paths = {}; + for (const auto& dir : m_asm_file_search_dirs) { + std::string base_dir = file_util::get_file_path({dir}); + const auto& results = file_util::find_files_recursively( + base_dir, std::regex(fmt::format("^{}(\\..*)?$", base_name))); + for (const auto& result : results) { + candidate_paths.push_back(result); + } + } + if (candidate_paths.empty()) { + lg::print("ERROR - attempt to find object file automatically, but found nothing\n"); + return; + } else if (candidate_paths.size() > 1) { + lg::print("ERROR - attempt to find object file automatically, but found multiple\n"); + return; + } + // Found the file!, use it! + file_path = candidate_paths.at(0).string(); + } + + auto code = m_goos.reader.read_from_file({file_path}); + + std::string obj_file_name = file_path; // Extract object name from file name. - for (int idx = int(options.filename.size()) - 1; idx-- > 0;) { - if (options.filename.at(idx) == '\\' || options.filename.at(idx) == '/') { - obj_file_name = options.filename.substr(idx + 1); + for (int idx = int(file_path.size()) - 1; idx-- > 0;) { + if (file_path.at(idx) == '\\' || file_path.at(idx) == '/') { + obj_file_name = file_path.substr(idx + 1); break; } } diff --git a/goalc/compiler/Compiler.h b/goalc/compiler/Compiler.h index 4754d3810..bd41b6336 100644 --- a/goalc/compiler/Compiler.h +++ b/goalc/compiler/Compiler.h @@ -81,9 +81,10 @@ class Compiler { listener::Listener& listener() { return m_listener; } void poke_target() { m_listener.send_poke(); } bool connect_to_target(); - Replxx::completions_t find_symbols_by_prefix(std::string const& context, - int& contextLen, - std::vector const& user_data); + Replxx::completions_t find_symbols_or_object_file_by_prefix( + std::string const& context, + int& contextLen, + std::vector const& user_data); Replxx::hints_t find_hints_by_prefix(std::string const& context, int& contextLen, Replxx::Color& color, @@ -93,7 +94,8 @@ class Compiler { std::vector> const& user_data); bool knows_object_file(const std::string& name); MakeSystem& make_system() { return m_make; } - void update_via_config_file(const std::string& json); + void update_via_config_file(const std::string& json, + const std::optional game_name = {}); private: GameVersion m_version; @@ -114,7 +116,10 @@ class Compiler { SymbolInfoMap m_symbol_info; std::unique_ptr m_repl; MakeSystem m_make; + + // Configurable fields int m_target_connect_attempts = 30; + std::vector m_asm_file_search_dirs = {}; struct DebugStats { int num_spills = 0; diff --git a/goalc/compiler/Env.cpp b/goalc/compiler/Env.cpp index 7e0ed6ff1..f241a20bb 100644 --- a/goalc/compiler/Env.cpp +++ b/goalc/compiler/Env.cpp @@ -108,6 +108,16 @@ FileEnv* GlobalEnv::add_file(std::string name) { return m_files.back().get(); } +std::vector GlobalEnv::list_files_with_prefix(const std::string& prefix) { + std::vector matches = {}; + for (const auto& file : m_files) { + if (file->name().rfind(prefix) == 0) { + matches.push_back(file->name()); + } + } + return matches; +} + /////////////////// // BlockEnv /////////////////// diff --git a/goalc/compiler/Env.h b/goalc/compiler/Env.h index 64a3ba7ce..8c43fb44b 100644 --- a/goalc/compiler/Env.h +++ b/goalc/compiler/Env.h @@ -84,6 +84,8 @@ class GlobalEnv : public Env { ~GlobalEnv() = default; FileEnv* add_file(std::string name); + // TODO - consider refactoring to use a Trie + std::vector list_files_with_prefix(const std::string& prefix); private: std::vector> m_files; diff --git a/goalc/compiler/Util.cpp b/goalc/compiler/Util.cpp index 11f4305d8..c0cb4536d 100644 --- a/goalc/compiler/Util.cpp +++ b/goalc/compiler/Util.cpp @@ -154,11 +154,20 @@ bool Compiler::knows_object_file(const std::string& name) { return m_debugger.knows_object(name); } -void Compiler::update_via_config_file(const std::string& json) { +void Compiler::update_via_config_file(const std::string& json, + const std::optional game_name) { auto cfg = parse_commented_json(json, "repl-config.json"); if (cfg.contains("numConnectToTargetAttempts")) { m_target_connect_attempts = cfg.at("numConnectToTargetAttempts").get(); } + if (cfg.contains("asmFileSearchDirs")) { + m_asm_file_search_dirs = cfg.at("asmFileSearchDirs").get>(); + } + // If there are any game specific config entries, set or override with them + if (game_name && cfg.contains(game_name.value())) { + auto game_cfg = cfg.at(game_name.value()).get(); + update_via_config_file(game_cfg.dump(), {}); + } } /*! diff --git a/goalc/compiler/compilation/CompilerControl.cpp b/goalc/compiler/compilation/CompilerControl.cpp index 20b4bbd4f..37a17f87c 100644 --- a/goalc/compiler/compilation/CompilerControl.cpp +++ b/goalc/compiler/compilation/CompilerControl.cpp @@ -9,6 +9,7 @@ #include "common/goos/ReplUtils.h" #include "common/util/DgoWriter.h" #include "common/util/FileUtil.h" +#include "common/util/StringUtil.h" #include "common/util/Timer.h" #include "goalc/compiler/Compiler.h" @@ -378,17 +379,42 @@ Val* Compiler::compile_get_info(const goos::Object& form, const goos::Object& re return get_none(); } -Replxx::completions_t Compiler::find_symbols_by_prefix(std::string const& context, - int& contextLen, - std::vector const& user_data) { +Replxx::completions_t Compiler::find_symbols_or_object_file_by_prefix( + std::string const& context, + int& contextLen, + std::vector const& user_data) { (void)contextLen; (void)user_data; - auto token = m_repl->get_current_repl_token(context); - auto possible_forms = lookup_symbol_infos_starting_with(token.first); Replxx::completions_t completions; - for (auto& x : possible_forms) { - completions.push_back(token.second ? "(" + x : x); + + // If we are trying to execute a `(ml ...)` we can automatically get the object file + // insert quotes if needed as well. + if (str_util::starts_with(context, "(ml ")) { + std::string file_name_prefix = context.substr(4); + // Trim string just incase, extra whitespace is valid LISP + file_name_prefix = str_util::trim(file_name_prefix); + // Remove quotes + file_name_prefix.erase(remove(file_name_prefix.begin(), file_name_prefix.end(), '"'), + file_name_prefix.end()); + if (file_name_prefix.empty()) { + return completions; + } + + // Get all the potential object file names + const auto& matches = m_global_env->list_files_with_prefix(file_name_prefix); + for (const auto& match : matches) { + completions.push_back(fmt::format("\"{}\")", match)); + } + } else { + const auto [token, stripped_leading_paren] = m_repl->get_current_repl_token(context); + // Otherwise, look for symbols + auto possible_forms = lookup_symbol_infos_starting_with(token); + + for (auto& x : possible_forms) { + completions.push_back(stripped_leading_paren ? "(" + x : x); + } } + return completions; } @@ -403,6 +429,8 @@ Replxx::hints_t Compiler::find_hints_by_prefix(std::string const& context, Replxx::hints_t hints; + // TODO - hints for `(ml ...` as well + // Only show hints if there are <= 3 possibilities if (possible_forms.size() <= 3) { for (auto& x : possible_forms) { diff --git a/goalc/main.cpp b/goalc/main.cpp index 8d0222ee9..1e57af409 100644 --- a/goalc/main.cpp +++ b/goalc/main.cpp @@ -134,7 +134,7 @@ int main(int argc, char** argv) { } compiler = std::make_unique(game_version, username, std::make_unique()); if (repl_config) { - compiler->update_via_config_file(repl_config.value()); + compiler->update_via_config_file(repl_config.value(), game); } // Start nREPL Server if (repl_server_ok) { @@ -180,7 +180,7 @@ int main(int argc, char** argv) { compiler = std::make_unique(game_version, username, std::make_unique()); if (repl_config) { - compiler->update_via_config_file(repl_config.value()); + compiler->update_via_config_file(repl_config.value(), game); } if (!startup_cmd.empty()) { compiler->handle_repl_string(startup_cmd);