diff --git a/docs/language_servers.md b/docs/language_servers.md index bf702cf..8d4b883 100644 --- a/docs/language_servers.md +++ b/docs/language_servers.md @@ -81,9 +81,7 @@ ln -s `which pyls` /usr/local/bin/python-language-server Install language server: ```sh -rustup component add rust-src -rustup toolchain install nightly -rustup component add --toolchain nightly rust-src rust-analyzer-preview +rustup toolchain install nightly --component rust-analyzer-preview ``` - Additional setup within a Rust project: diff --git a/src/debug_lldb.cpp b/src/debug_lldb.cpp index 327f6c3..fe5023d 100644 --- a/src/debug_lldb.cpp +++ b/src/debug_lldb.cpp @@ -1,8 +1,4 @@ #include "debug_lldb.hpp" -#include -#ifdef __APPLE__ -#include -#endif #include "config.hpp" #include "filesystem.hpp" #include "process.hpp" @@ -20,18 +16,11 @@ void log(const char *msg, void *) { } Debug::LLDB::LLDB() : state(lldb::StateType::eStateInvalid), buffer_size(131072) { - if(!getenv("LLDB_DEBUGSERVER_PATH")) { #ifndef __APPLE__ - auto debug_server_path = filesystem::get_executable("lldb-server").string(); - if(debug_server_path != "lldb-server") { -#ifdef _WIN32 - Glib::setenv("LLDB_DEBUGSERVER_PATH", debug_server_path.c_str(), 0); -#else - setenv("LLDB_DEBUGSERVER_PATH", debug_server_path.c_str(), 0); + auto debug_server_path = filesystem::get_executable("lldb-server").string(); + if(debug_server_path != "lldb-server") + Glib::setenv("LLDB_DEBUGSERVER_PATH", debug_server_path, false); #endif - } -#endif - } } void Debug::LLDB::destroy_() { diff --git a/src/filesystem.cpp b/src/filesystem.cpp index f28fa85..8255643 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -6,6 +6,10 @@ #include #include +boost::optional filesystem::rust_sysroot_path; +boost::optional filesystem::rust_nightly_sysroot_path; +boost::optional> filesystem::executable_search_paths; + //Only use on small files std::string filesystem::read(const std::string &path) { std::string str; @@ -82,8 +86,8 @@ std::string filesystem::unescape_argument(const std::string &argument) { return unescaped; } -boost::filesystem::path filesystem::get_current_path() noexcept { - auto current_path = [] { +const boost::filesystem::path &filesystem::get_current_path() noexcept { + auto get_path = [] { #ifdef _WIN32 boost::system::error_code ec; auto path = boost::filesystem::current_path(ec); @@ -104,12 +108,12 @@ boost::filesystem::path filesystem::get_current_path() noexcept { #endif }; - static boost::filesystem::path path = current_path(); + static boost::filesystem::path path = get_path(); return path; } -boost::filesystem::path filesystem::get_home_path() noexcept { - auto home_path = [] { +const boost::filesystem::path &filesystem::get_home_path() noexcept { + auto get_path = [] { std::vector environment_variables = {"HOME", "AppData"}; for(auto &variable : environment_variables) { if(auto ptr = std::getenv(variable.c_str())) { @@ -122,12 +126,12 @@ boost::filesystem::path filesystem::get_home_path() noexcept { return boost::filesystem::path(); }; - static boost::filesystem::path path = home_path(); + static boost::filesystem::path path = get_path(); return path; } -boost::filesystem::path filesystem::get_rust_sysroot_path() noexcept { - auto rust_sysroot_path = [] { +const boost::filesystem::path &filesystem::get_rust_sysroot_path() noexcept { + auto get_path = [] { std::string path; TinyProcessLib::Process process( "rustc --print sysroot", "", @@ -143,17 +147,32 @@ boost::filesystem::path filesystem::get_rust_sysroot_path() noexcept { return boost::filesystem::path(); }; - static boost::filesystem::path path = rust_sysroot_path(); - return path; + if(!rust_sysroot_path) + rust_sysroot_path = get_path(); + return *rust_sysroot_path; } boost::filesystem::path filesystem::get_rust_nightly_sysroot_path() noexcept { - auto path = get_rust_sysroot_path(); - if(path.empty()) - return {}; - auto filename = path.filename().string(); - auto pos = filename.find('-'); - return path.parent_path() / (pos != std::string::npos ? "nightly" + filename.substr(pos) : "nightly"); + auto get_path = [] { + std::string path; + TinyProcessLib::Process process( + // Slightly complicated since "RUSTUP_TOOLCHAIN=nightly rustc --print sysroot" actually installs nightly toolchain if missing... + "rustup toolchain list|grep nightly > /dev/null && RUSTUP_TOOLCHAIN=nightly rustc --print sysroot", "", + [&path](const char *buffer, size_t length) { + path += std::string(buffer, length); + }, + [](const char *buffer, size_t n) {}); + if(process.get_exit_status() == 0) { + while(!path.empty() && (path.back() == '\n' || path.back() == '\r')) + path.pop_back(); + return boost::filesystem::path(path); + } + return boost::filesystem::path(); + }; + + if(!rust_nightly_sysroot_path) + rust_nightly_sysroot_path = get_path(); + return *rust_nightly_sysroot_path; } boost::filesystem::path filesystem::get_short_path(const boost::filesystem::path &path) noexcept { @@ -299,10 +318,13 @@ boost::filesystem::path filesystem::get_executable(const boost::filesystem::path // Based on https://stackoverflow.com/a/11295568 const std::vector &filesystem::get_executable_search_paths() { - auto executable_search_paths = [] { + auto get_paths = [] { std::vector paths; - const std::string env = getenv("PATH"); + auto c_env = std::getenv("PATH"); + if(!c_env) + return paths; + const std::string env = c_env; const char delimiter = ':'; size_t previous = 0; @@ -316,8 +338,9 @@ const std::vector &filesystem::get_executable_search_pa return paths; }; - static std::vector paths = executable_search_paths(); - return paths; + if(!executable_search_paths) + executable_search_paths = get_paths(); + return *executable_search_paths; } boost::filesystem::path filesystem::find_executable(const std::string &executable_name) { diff --git a/src/filesystem.hpp b/src/filesystem.hpp index 41955ca..dc39a2f 100644 --- a/src/filesystem.hpp +++ b/src/filesystem.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -17,13 +18,17 @@ public: static std::string unescape_argument(const std::string &argument); /// Does not resolve symbolic links. Returns empty path on failure. - static boost::filesystem::path get_current_path() noexcept; + static const boost::filesystem::path &get_current_path() noexcept; /// Returns empty path on failure - static boost::filesystem::path get_home_path() noexcept; + static const boost::filesystem::path &get_home_path() noexcept; /// Returns empty path on failure - static boost::filesystem::path get_rust_sysroot_path() noexcept; + static const boost::filesystem::path &get_rust_sysroot_path() noexcept; + /// Set to {} to reset get_rust_sysroot_path + static boost::optional rust_sysroot_path; /// Returns empty path on failure static boost::filesystem::path get_rust_nightly_sysroot_path() noexcept; + /// Set to {} to reset get_rust_sysroot_path + static boost::optional rust_nightly_sysroot_path; /// Replaces home path with ~ static boost::filesystem::path get_short_path(const boost::filesystem::path &path) noexcept; /// Replaces ~ with home path (boost::filesystem does not recognize ~) @@ -43,6 +48,8 @@ public: static boost::filesystem::path get_executable(const boost::filesystem::path &executable_name) noexcept; static const std::vector &get_executable_search_paths(); + /// Set to {} to reset get_executable_search_paths + static boost::optional> executable_search_paths; /// Returns full executable path if found, or empty path otherwise. static boost::filesystem::path find_executable(const std::string &executable_name); diff --git a/src/notebook.cpp b/src/notebook.cpp index 70d4e50..51cdeb6 100644 --- a/src/notebook.cpp +++ b/src/notebook.cpp @@ -68,6 +68,21 @@ Notebook::Notebook() : Gtk::Paned(), notebooks(2) { }); } pack1(notebooks[0], true, true); + + if(filesystem::find_executable("rustup").empty()) { + // PATH might not be set (for instance after installation) + auto cargo_bin = filesystem::get_home_path() / ".cargo" / "bin"; + boost::system::error_code ec; + if(boost::filesystem::is_directory(cargo_bin, ec)) { + std::string env; + if(auto c_env = std::getenv("PATH")) + env = c_env; + Glib::setenv("PATH", !env.empty() ? env + ':' + cargo_bin.string() : cargo_bin.string()); + filesystem::rust_sysroot_path = {}; + filesystem::rust_nightly_sysroot_path = {}; + filesystem::executable_search_paths = {}; + } + } } size_t Notebook::size() { @@ -179,70 +194,83 @@ bool Notebook::open(const boost::filesystem::path &file_path_, Position position if(!filesystem::find_executable("rust-analyzer").empty()) source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, "rust-analyzer")); else { - auto sysroot = filesystem::get_rust_sysroot_path(); - if(!sysroot.empty()) { - auto rust_analyzer = sysroot / "bin" / "rust-analyzer"; - boost::system::error_code ec; - if(boost::filesystem::exists(rust_analyzer, ec)) - source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument(rust_analyzer.string()))); - else { - auto nightly_sysroot = filesystem::get_rust_nightly_sysroot_path(); - if(!nightly_sysroot.empty()) { - auto nightly_rust_analyzer = nightly_sysroot / "bin" / "rust-analyzer"; - boost::system::error_code ec; - if(boost::filesystem::exists(nightly_rust_analyzer, ec)) - source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument(nightly_rust_analyzer.string()))); + if(filesystem::find_executable("rustup").empty()) + install_rust(); + if(!filesystem::find_executable("rustup").empty()) { + // Try find rust-analyzer installed with rustup + auto sysroot = filesystem::get_rust_sysroot_path(); + if(!sysroot.empty()) { + auto rust_analyzer = sysroot / "bin" / "rust-analyzer"; + boost::system::error_code ec; + if(boost::filesystem::exists(rust_analyzer, ec)) + source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument(rust_analyzer.string()))); + else { + // Workaround while rust-analyzer is in nightly toolchain only + auto nightly_sysroot = filesystem::get_rust_nightly_sysroot_path(); + if(!nightly_sysroot.empty()) { + auto nightly_rust_analyzer = nightly_sysroot / "bin" / "rust-analyzer"; + boost::system::error_code ec; + if(boost::filesystem::exists(nightly_rust_analyzer, ec)) + source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument(nightly_rust_analyzer.string()))); + } } - if(source_views_previous_size == source_views.size()) { - auto install_rust_analyzer = [this](const std::string &command) { - Gtk::MessageDialog dialog(*static_cast(get_toplevel()), "Install rust-analyzer (Rust language server)", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); - dialog.set_default_response(Gtk::RESPONSE_YES); - dialog.set_secondary_text("Would you like to install rust-analyzer through rustup?"); - int result = dialog.run(); - dialog.hide(); - if(result == Gtk::RESPONSE_YES) { - bool canceled = false; - Dialog::Message message("Installing rust-analyzer", [&canceled] { - canceled = true; - }); - boost::optional exit_status; - auto process = Terminal::get().async_process(std::string(command), "", [&exit_status](int exit_status_) { - exit_status = exit_status_; - }); - bool killed = false; - while(!exit_status) { - if(canceled && !killed) { - process->kill(); - killed = true; - } - while(Gtk::Main::events_pending()) - Gtk::Main::iteration(); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - if(exit_status == 0) { - Terminal::get().print("\e[32mSuccessfully\e[m installed rust-analyzer.\n"); - return true; + } + if(source_views_previous_size == source_views.size()) { + // Install rust-analyzer + auto install_rust_analyzer = [this](const std::string &command) { + Gtk::MessageDialog dialog(*static_cast(get_toplevel()), "Install Rust language server", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); + dialog.set_default_response(Gtk::RESPONSE_YES); + dialog.set_secondary_text("Rust language server not found. Would you like to install rust-analyzer?"); + int result = dialog.run(); + dialog.hide(); + if(result == Gtk::RESPONSE_YES) { + bool canceled = false; + Dialog::Message message("Installing rust-analyzer", [&canceled] { + canceled = true; + }); + boost::optional exit_status; + + Terminal::get().print("\e[2mRunning: " + command + "\e[m\n"); + auto process = Terminal::get().async_process(command, "", [&exit_status](int exit_status_) { + exit_status = exit_status_; + }); + bool killed = false; + while(!exit_status) { + if(canceled && !killed) { + process->kill(); + killed = true; } + while(Gtk::Main::events_pending()) + Gtk::Main::iteration(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if(exit_status == 0) { + Terminal::get().print("\e[32mSuccess\e[m: installed rust-analyzer\n"); + return true; } - return false; - }; - static bool first = true; - if(first && !filesystem::find_executable("rustup").empty()) { - first = false; - std::stringstream stdin_stream, stdout_stream; - if(Terminal::get().process(stdin_stream, stdout_stream, "rustup component list") == 0) { - bool rust_analyzer_in_toolchain = false; - std::string line; - while(std::getline(stdout_stream, line)) { - if(starts_with(line, "rust-analyzer")) { - if(install_rust_analyzer(std::string("rustup component add rust-src ") + (starts_with(line, "rust-analyzer-preview") ? "rust-analyzer-preview" : "rust-analyzer"))) - source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument(rust_analyzer.string()))); - rust_analyzer_in_toolchain = true; - break; - } + } + return false; + }; + static bool first = true; + if(first) { + first = false; + std::stringstream stdin_stream, stdout_stream; + // Workaround while rust-analyzer is called rust-analyzer-preview instead of rust-analyzer + if(Terminal::get().process(stdin_stream, stdout_stream, "rustup component list") == 0) { + bool rust_analyzer_in_toolchain = false; + std::string line; + while(std::getline(stdout_stream, line)) { + if(starts_with(line, "rust-analyzer")) { + if(install_rust_analyzer(std::string("rustup component add ") + (starts_with(line, "rust-analyzer-preview") ? "rust-analyzer-preview" : "rust-analyzer"))) + source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument((filesystem::get_rust_sysroot_path() / "bin" / "rust-analyzer").string()))); + rust_analyzer_in_toolchain = true; + break; } - if(!rust_analyzer_in_toolchain && install_rust_analyzer("rustup component add rust-src && rustup toolchain install nightly && rustup component add --toolchain nightly rust-src rust-analyzer-preview")) - source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, "rustup run nightly rust-analyzer")); + } + // Workaround while rust-analyzer is in nightly toolchain only + if(!rust_analyzer_in_toolchain && install_rust_analyzer("rustup toolchain install nightly --component rust-analyzer-preview")) { + filesystem::rust_nightly_sysroot_path = {}; + source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id, filesystem::escape_argument((filesystem::get_rust_nightly_sysroot_path() / "bin" / "rust-analyzer").string()))); } } } @@ -266,15 +294,12 @@ bool Notebook::open(const boost::filesystem::path &file_path_, Position position } else if(language_id == "rust") { auto rust_installed = !filesystem::get_rust_sysroot_path().empty(); - if(!rust_installed) { - Terminal::get().print("\e[33mWarning\e[m: could not find Rust. You can install Rust by running the following command in a terminal:\n\n"); - Terminal::get().print("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n"); + Terminal::get().print(std::string("\e[33mWarning\e[m: could not find Rust") + (rust_installed ? " language server" : "") + ".\n"); + Terminal::get().print("For installation instructions please visit: https://gitlab.com/cppit/jucipp/-/blob/master/docs/language_servers.md#rust.\n"); + if(!rust_installed) Terminal::get().print("You will need to restart juCi++ after installing Rust.\n"); - } - else { - Terminal::get().print("\e[33mWarning\e[m: could not find Rust language server.\n"); - Terminal::get().print("For installation instructions please visit: https://gitlab.com/cppit/jucipp/-/blob/master/docs/language_servers.md#rust.\n"); - } + shown.emplace(language_id); + shown.emplace(language_id); } else if(language_id == "go") { @@ -579,6 +604,54 @@ bool Notebook::open(const boost::filesystem::path &file_path_, Position position return true; } +void Notebook::install_rust() { + static bool first = true; + if(first) { + first = false; + Gtk::MessageDialog dialog(*static_cast(get_toplevel()), "Install Rust", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); + dialog.set_default_response(Gtk::RESPONSE_YES); + dialog.set_secondary_text("Rust not found. Would you like to install Rust?"); + int result = dialog.run(); + dialog.hide(); + if(result == Gtk::RESPONSE_YES) { + bool canceled = false; + Dialog::Message message("Installing Rust", [&canceled] { + canceled = true; + }); + boost::optional exit_status; + + std::string command = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"; + Terminal::get().print("\e[2mRunning: " + command + "\e[m\n"); + auto process = Terminal::get().async_process(command, "", [&exit_status](int exit_status_) { + exit_status = exit_status_; + }); + bool killed = false; + while(!exit_status) { + if(canceled && !killed) { + process->kill(); + killed = true; + } + while(Gtk::Main::events_pending()) + Gtk::Main::iteration(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if(exit_status == 0) { + if(filesystem::find_executable("rustup").empty()) { + std::string env; + if(auto c_env = std::getenv("PATH")) + env = c_env; + auto cargo_bin = filesystem::get_home_path() / ".cargo" / "bin"; + Glib::setenv("PATH", !env.empty() ? env + ':' + cargo_bin.string() : cargo_bin.string()); + } + filesystem::rust_sysroot_path = {}; + filesystem::rust_nightly_sysroot_path = {}; + filesystem::executable_search_paths = {}; + Terminal::get().print("\e[32mSuccess\e[m: installed Rust and updated PATH environment variable\n"); + } + } + } +} + void Notebook::open_uri(const std::string &uri) { #ifdef __APPLE__ Terminal::get().process("open " + filesystem::escape_argument(uri)); diff --git a/src/notebook.hpp b/src/notebook.hpp index 6b30171..cda86ae 100644 --- a/src/notebook.hpp +++ b/src/notebook.hpp @@ -45,6 +45,7 @@ public: }; bool open(Source::View *view); bool open(const boost::filesystem::path &file_path, Position position = Position::infer); + void install_rust(); void open_uri(const std::string &uri); void configure(size_t index); bool save(size_t index); diff --git a/src/window.cpp b/src/window.cpp index 171e3b0..c8b0830 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -381,9 +381,9 @@ void Window::set_menu_actions() { Directories::get().open(project_path); Notebook::get().open(c_main_path); Directories::get().update(); - Terminal::get().print("C project "); + Terminal::get().print("\e[32mSuccess\e[m: created C project "); Terminal::get().print(project_name, true); - Terminal::get().print(" \e[32mcreated\e[m\n"); + Terminal::get().print("\n"); } else Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); @@ -438,19 +438,20 @@ void Window::set_menu_actions() { Directories::get().open(project_path); Notebook::get().open(cpp_main_path); Directories::get().update(); - Terminal::get().print("C++ project "); + Terminal::get().print("\e[32mSuccess\e[m: created C++ project "); Terminal::get().print(project_name, true); - Terminal::get().print(" \e[32mcreated\e[m\n"); + Terminal::get().print("\n"); } else Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); } }); menu.add_action("file_new_project_rust", []() { - auto sysroot = filesystem::get_rust_sysroot_path(); - if(sysroot.empty()) { - Terminal::get().print("\e[31mError\e[m: could not find Rust. You can install Rust by running the following command in a terminal:\n\n"); - Terminal::get().print("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n"); + if(filesystem::find_executable("rustup").empty()) + Notebook::get().install_rust(); + if(filesystem::find_executable("rustup").empty()) { + Terminal::get().print("\e[33mWarning\e[m: could not find Rust.\n"); + Terminal::get().print("For installation instructions please visit: https://gitlab.com/cppit/jucipp/-/blob/master/docs/language_servers.md#rust.\n"); Terminal::get().print("You will need to restart juCi++ after installing Rust.\n"); return; } @@ -467,9 +468,9 @@ void Window::set_menu_actions() { Directories::get().open(project_path); Notebook::get().open(project_path / "src" / "main.rs"); Directories::get().update(); - Terminal::get().print("Rust project "); - Terminal::get().print(project_path.filename().string(), true); - Terminal::get().print(" \e[32mcreated\e[m\n"); + Terminal::get().print("\e[32mSuccess\e[m: created Rust project "); + Terminal::get().print(project_name, true); + Terminal::get().print("\n"); } else Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true);