diff --git a/src/source.cpp b/src/source.cpp index df12e6b..44f8584 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -33,6 +33,8 @@ inline pid_t get_current_process_id() { } #endif +std::unique_ptr Source::View::prettier_background_process = {}; + Glib::RefPtr Source::LanguageManager::get_default() { static auto instance = Gsv::LanguageManager::create(); return instance; @@ -748,7 +750,6 @@ void Source::View::setup_format_style(bool is_generic_view) { }); } format_style = [this, is_generic_view](bool continue_without_style_file) { - auto command = prettier.string(); if(!continue_without_style_file) { auto search_path = file_path.parent_path(); while(true) { @@ -779,63 +780,240 @@ void Source::View::setup_format_style(bool is_generic_view) { } } - command += " --stdin-filepath " + filesystem::escape_argument(this->file_path.string()); - - if(get_buffer()->get_has_selection()) { // Cannot be used together with --cursor-offset - Gtk::TextIter start, end; - get_buffer()->get_selection_bounds(start, end); - command += " --range-start " + std::to_string(start.get_offset()); - command += " --range-end " + std::to_string(end.get_offset()); - } - else - command += " --cursor-offset " + std::to_string(get_buffer()->get_insert()->get_iter().get_offset()); - size_t num_warnings = 0, num_errors = 0, num_fix_its = 0; if(is_generic_view) clear_diagnostic_tooltips(); - std::stringstream stdin_stream(get_buffer()->get_text()), stdout_stream, stderr_stream; - auto exit_status = Terminal::get().process(stdin_stream, stdout_stream, command, this->file_path.parent_path(), &stderr_stream); - if(exit_status == 0) { - replace_text(stdout_stream.str()); - std::string line; - std::getline(stderr_stream, line); - if(!line.empty() && line != "NaN") { - try { - auto offset = std::stoi(line); - if(offset < get_buffer()->size()) { - get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(offset)); + static auto get_prettier_library = [] { + std::string library; + TinyProcessLib::Process process( + "npm root -g", "", + [&library](const char *buffer, size_t length) { + library += std::string(buffer, length); + }, + [](const char *, size_t) {}); + if(process.get_exit_status() == 0) { + while(!library.empty() && (library.back() == '\n' || library.back() == '\r')) + library.pop_back(); + library += "/prettier"; + boost::system::error_code ec; + if(boost::filesystem::is_directory(library, ec)) + return library; + else { + auto parent_path = prettier.parent_path(); + if(parent_path.filename() == "bin") { + auto path = parent_path.parent_path() / "lib" / "prettier"; + if(boost::filesystem::is_directory(path, ec)) + return path.string(); + } + // Try find prettier library installed with homebrew on MacOS + boost::filesystem::path path = "/usr/local/opt/prettier/libexec/lib/node_modules/prettier"; + if(boost::filesystem::is_directory(path, ec)) + return path.string(); + path = "/opt/homebrew/opt/prettier/libexec/lib/node_modules/prettier"; + if(boost::filesystem::is_directory(path, ec)) + return path.string(); + } + } + return std::string(); + }; + static auto prettier_library = get_prettier_library(); + + auto buffer = get_buffer()->get_text().raw(); + if(!prettier_library.empty() && buffer.size() < 25000) { // Node.js repl seems to be limited to around 28786 bytes + struct Error { + std::string message; + int line = -1, index = -1; + }; + struct Result { + std::string text; + int cursor_offset; + }; + static Mutex mutex; + static boost::optional result GUARDED_BY(mutex); + static boost::optional error GUARDED_BY(mutex); + { + LockGuard lock(mutex); + result = {}; + error = {}; + } + if(prettier_background_process) { + int exit_status; + if(prettier_background_process->try_get_exit_status(exit_status)) + prettier_background_process = {}; + } + if(!prettier_background_process) { + prettier_background_process = std::make_unique( + "node -e \"const repl = require('repl');repl.start({prompt: '', ignoreUndefined: true, preview: false});\"", + "", + [](const char *bytes, size_t n) { + try { + JSON json(std::string(bytes, n)); + LockGuard lock(mutex); + result = Result{json.string("formatted"), static_cast(json.integer_or("cursorOffset", -1))}; + } + catch(const std::exception &e) { + LockGuard lock(mutex); + error = Error{e.what()}; + error->message += "\nOutput from prettier: " + std::string(bytes, n); + } + }, + [](const char *bytes, size_t n) { + size_t i = 0; + for(; i < n; ++i) { + if(bytes[i] == '\n') + break; + } + std::string first_line(bytes, i); + std::string message; + int line = -1, line_index = -1; + + if(starts_with(first_line, "ConfigError: ")) + message = std::string(bytes + 13, n - 13); + else if(starts_with(first_line, "ParseError: ")) { + const static std::regex regex(R"(^(.*) \(([0-9]*):([0-9]*)\)$)", std::regex::optimize); + std::smatch sm; + first_line.erase(0, 12); + if(std::regex_match(first_line, sm, regex)) { + message = sm[1].str(); + try { + line = std::stoi(sm[2].str()); + line_index = std::stoi(sm[3].str()); + } + catch(...) { + line = -1; + line_index = -1; + } + } + else + message = std::string(bytes + 12, n - 12); + } + else + message = std::string(bytes, n); + + LockGuard lock(mutex); + error = Error{std::move(message), line, line_index}; + }, + true, TinyProcessLib::Config{1048576}); + + prettier_background_process->write("const prettier = require(\"" + escape(prettier_library, {'"'}) + "\");\n"); + } + + std::string options = "filepath: \"" + escape(file_path.string(), {'"'}) + "\""; + if(get_buffer()->get_has_selection()) { // Cannot be used together with cursorOffset + Gtk::TextIter start, end; + get_buffer()->get_selection_bounds(start, end); + options += ", rangeStart: " + std::to_string(start.get_offset()) + ", rangeEnd: " + std::to_string(end.get_offset()); + } + else + options += ", cursorOffset: " + std::to_string(get_buffer()->get_insert()->get_iter().get_offset()); + prettier_background_process->write("{prettier.clearConfigCache();let _ = prettier.resolveConfig(\"" + escape(file_path.string(), {'"'}) + "\").then(options => {try{let _ = process.stdout.write(JSON.stringify(prettier.formatWithCursor(Buffer.from('"); + prettier_background_process->write(to_hex_string(buffer)); + buffer.clear(); + prettier_background_process->write("', 'hex').toString(), {...options, " + options + "})));}catch(error){let _ = process.stderr.write('ParseError: ' + error.message);}}).catch(error => {let _ = process.stderr.write('ConfigError: ' + error.message);});}\n"); + + while(true) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + int exit_status; + if(prettier_background_process->try_get_exit_status(exit_status)) + break; + LockGuard lock(mutex); + if(result || error) + break; + } + { + LockGuard lock(mutex); + if(result) { + replace_text(result->text); + if(result->cursor_offset >= 0 && result->cursor_offset < get_buffer()->size()) { + get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(result->cursor_offset)); hide_tooltips(); } } - catch(...) { + else if(error) { + if(error->line != -1 && error->index != -1) { + if(is_generic_view) { + auto start = get_iter_at_line_offset(error->line - 1, error->index - 1); + ++num_errors; + while(start.ends_line() && start.backward_char()) { + } + auto end = start; + end.forward_char(); + if(start == end) + start.backward_char(); + + add_diagnostic_tooltip(start, end, true, [error_message = std::move(error->message)](Tooltip &tooltip) { + tooltip.insert_with_links_tagged(error_message); + }); + } + } + else + Terminal::get().print("\e[31mError (prettier)\e[m: " + error->message + '\n', true); } } } - else if(is_generic_view) { - const static std::regex regex(R"(^\[.*error.*\] [^:]*: (.*) \(([0-9]*):([0-9]*)\)$)", std::regex::optimize); - std::string line; - std::getline(stderr_stream, line); - std::smatch sm; - if(std::regex_match(line, sm, regex)) { - try { - auto start = get_iter_at_line_offset(std::stoi(sm[2].str()) - 1, std::stoi(sm[3].str()) - 1); - ++num_errors; - while(start.ends_line() && start.backward_char()) { - } - auto end = start; - end.forward_char(); - if(start == end) - start.backward_char(); + else { + auto command = prettier.string(); + command += " --stdin-filepath " + filesystem::escape_argument(this->file_path.string()); - add_diagnostic_tooltip(start, end, true, [error_message = sm[1].str()](Tooltip &tooltip) { - tooltip.insert_with_links_tagged(error_message); - }); + if(get_buffer()->get_has_selection()) { // Cannot be used together with --cursor-offset + Gtk::TextIter start, end; + get_buffer()->get_selection_bounds(start, end); + command += " --range-start " + std::to_string(start.get_offset()); + command += " --range-end " + std::to_string(end.get_offset()); + } + else + command += " --cursor-offset " + std::to_string(get_buffer()->get_insert()->get_iter().get_offset()); + + std::stringstream stdin_stream(buffer), stdout_stream, stderr_stream; + buffer.clear(); + auto exit_status = Terminal::get().process(stdin_stream, stdout_stream, command, this->file_path.parent_path(), &stderr_stream); + if(exit_status == 0) { + replace_text(stdout_stream.str()); + std::string line; + std::getline(stderr_stream, line); + if(!line.empty() && line != "NaN") { + try { + auto offset = std::stoi(line); + if(offset < get_buffer()->size()) { + get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(offset)); + hide_tooltips(); + } + } + catch(...) { + } } - catch(...) { + } + else { + const static std::regex regex(R"(^\[.*error.*\] [^:]*: (.*) \(([0-9]*):([0-9]*)\)$)", std::regex::optimize); + std::string line; + std::getline(stderr_stream, line); + std::smatch sm; + if(std::regex_match(line, sm, regex)) { + if(is_generic_view) { + try { + auto start = get_iter_at_line_offset(std::stoi(sm[2].str()) - 1, std::stoi(sm[3].str()) - 1); + ++num_errors; + while(start.ends_line() && start.backward_char()) { + } + auto end = start; + end.forward_char(); + if(start == end) + start.backward_char(); + + add_diagnostic_tooltip(start, end, true, [error_message = sm[1].str()](Tooltip &tooltip) { + tooltip.insert_with_links_tagged(error_message); + }); + } + catch(...) { + } + } } + else + Terminal::get().print("\e[31mError (prettier)\e[m: " + stderr_stream.str(), true); } } + if(is_generic_view) { status_diagnostics = std::make_tuple(num_warnings, num_errors, num_fix_its); if(update_status_diagnostics) diff --git a/src/source.hpp b/src/source.hpp index 1369486..93edaf2 100644 --- a/src/source.hpp +++ b/src/source.hpp @@ -1,4 +1,5 @@ #pragma once +#include "process.hpp" #include "source_diff.hpp" #include "source_spellcheck.hpp" #include "tooltips.hpp" @@ -115,6 +116,8 @@ namespace Source { virtual void soft_reparse(bool delayed = false) { soft_reparse_needed = false; } virtual void full_reparse() { full_reparse_needed = false; } + static std::unique_ptr prettier_background_process; + protected: std::atomic parsed = {true}; Tooltips diagnostic_tooltips; diff --git a/src/utility.cpp b/src/utility.cpp index cc5938e..94dbf5d 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -1,4 +1,5 @@ #include "utility.hpp" +#include #include ScopeGuard::~ScopeGuard() { @@ -169,3 +170,25 @@ bool ends_with(const std::string &str, const char *test) noexcept { return false; return str.compare(str.size() - test_size, test_size, test) == 0; } + +std::string escape(const std::string &input, const std::set &escape_chars) { + std::string result; + result.reserve(input.size()); + for(auto &chr : input) { + if(escape_chars.find(chr) != escape_chars.end()) + result += '\\'; + result += chr; + } + return result; +} + +std::string to_hex_string(const std::string &input) { + std::string result; + result.reserve(input.size() * 2); + std::string hex_chars = "0123456789abcdef"; + for(auto &chr : input) { + result += hex_chars[static_cast(chr) >> 4]; + result += hex_chars[static_cast(chr) & 0x0f]; + } + return result; +} diff --git a/src/utility.hpp b/src/utility.hpp index 6faa5d3..7d2281f 100644 --- a/src/utility.hpp +++ b/src/utility.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include class ScopeGuard { @@ -27,3 +28,7 @@ bool starts_with(const std::string &str, size_t pos, const char *test) noexcept; bool ends_with(const std::string &str, const std::string &test) noexcept; bool ends_with(const std::string &str, const char *test) noexcept; + +std::string escape(const std::string &input, const std::set &escape_chars); + +std::string to_hex_string(const std::string &input); diff --git a/src/window.cpp b/src/window.cpp index 3910e25..0e463d7 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1999,6 +1999,12 @@ bool Window::on_delete_event(GdkEventAny *event) { } Terminal::get().kill_async_processes(); + if(Source::View::prettier_background_process) { + int exit_status; + if(!Source::View::prettier_background_process->try_get_exit_status(exit_status)) + Source::View::prettier_background_process->kill(); + } + #ifdef JUCI_ENABLE_DEBUG Debug::LLDB::destroy(); #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 08fcbf8..e081405 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ endif() add_definitions(-DJUCI_BUILD_PATH="${CMAKE_BINARY_DIR}" -DJUCI_TESTS_PATH="${CMAKE_CURRENT_SOURCE_DIR}") include_directories(${CMAKE_SOURCE_DIR}/src) +include_directories(${CMAKE_SOURCE_DIR}/lib/tiny-process-library) add_library(test_stubs OBJECT stubs/config.cpp