diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fda7ca3..66d7d1c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ set(JUCI_SHARED_FILES source.cpp source_base.cpp source_clang.cpp + source_coverage.cpp source_diff.cpp source_generic.cpp source_language_protocol.cpp diff --git a/src/config.cpp b/src/config.cpp index 41804b6..97aaf10 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -408,6 +408,7 @@ std::string Config::default_config() { "source_implement_method": "m", "source_goto_next_diagnostic": "e", "source_apply_fix_its": "space", + "source_toggle_coverage": "c", "project_set_run_arguments": "", "project_compile_and_run": "Return", "project_compile": "Return", diff --git a/src/menu.cpp b/src/menu.cpp index 2f819a7..e8eb89e 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -399,6 +399,12 @@ const Glib::ustring menu_xml = R"RAW( app.source_apply_fix_its +
+ + _Toggle _Coverage + app.source_toggle_coverage + +
diff --git a/src/source.cpp b/src/source.cpp index 573f135..02ac5ff 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -142,7 +142,7 @@ std::string Source::FixIt::string(BaseView &view) { std::set Source::View::non_deleted_views; std::set Source::View::views; -Source::View::View(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view) : BaseView(file_path, language), SpellCheckView(file_path, language), DiffView(file_path, language) { +Source::View::View(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view) : BaseView(file_path, language), SpellCheckView(file_path, language), DiffView(file_path, language), CoverageView(file_path, language) { non_deleted_views.emplace(this); views.emplace(this); @@ -463,6 +463,7 @@ bool Source::View::save() { void Source::View::configure() { SpellCheckView::configure(); DiffView::configure(); + CoverageView::configure(); if(Config::get().source.style.size() > 0) { auto scheme = StyleSchemeManager::get_default()->get_scheme(Config::get().source.style); diff --git a/src/source.hpp b/src/source.hpp index 2a76e7a..038046f 100644 --- a/src/source.hpp +++ b/src/source.hpp @@ -1,5 +1,6 @@ #pragma once #include "process.hpp" +#include "source_coverage.hpp" #include "source_diff.hpp" #include "source_spellcheck.hpp" #include "tooltips.hpp" @@ -72,7 +73,7 @@ namespace Source { } }; - class View : public SpellCheckView, public DiffView { + class View : public SpellCheckView, public DiffView, public CoverageView { public: static std::set non_deleted_views; static std::set views; diff --git a/src/source_clang.cpp b/src/source_clang.cpp index 9550ae1..f71deb0 100644 --- a/src/source_clang.cpp +++ b/src/source_clang.cpp @@ -2003,6 +2003,36 @@ Source::ClangViewRefactor::ClangViewRefactor(const boost::filesystem::path &file return std::tuple(Source::Offset(), "", 0); } }; + + get_coverage = [this]() { + auto build = Project::Build::create(this->file_path); + if(build->project_path.empty()) { + Info::get().print(this->file_path.filename().string() + ": could not find a supported build system"); + return std::vector{}; + } + build->update_default(); + CompileCommands commands(build->get_default_path()); + boost::filesystem::path object_file; + for(const auto &command : commands.commands) { + if(command.file == this->file_path) { + auto values = command.parameter_values("-o"); + if(!values.empty()) { + object_file = command.directory / values.front(); + break; + } + } + } + if(object_file.empty()) { + Info::get().print(this->file_path.filename().string() + ": could not find the C/C++ object file"); + return std::vector{}; + } + + auto result = Coverage::analyze(Coverage::FileInfo{this->file_path, object_file, build->get_default_path(), this->language_id}); + if(result.empty()) { + Info::get().print(this->file_path.filename().string() + ": no supported coverage information found"); + } + return result; + }; } Source::ClangViewRefactor::Identifier Source::ClangViewRefactor::get_identifier() { diff --git a/src/source_coverage.cpp b/src/source_coverage.cpp new file mode 100644 index 0000000..1eb2975 --- /dev/null +++ b/src/source_coverage.cpp @@ -0,0 +1,141 @@ +#include "source_coverage.hpp" +#include +#include + +Source::CoverageView::Renderer::Renderer() : Gsv::GutterRendererText() { + set_padding(4, 0); +} + +Source::CoverageView::CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), renderer(new Renderer()) {} + +Source::CoverageView::~CoverageView() { + get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->remove(renderer.get()); +} + +void Source::CoverageView::configure() { + // Set colors + auto &yellow = renderer->yellow; + auto &red = renderer->red; + auto &green = renderer->green; + auto &transparent = renderer->transparent; + auto normal_color = get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_NORMAL); + auto light_theme = (normal_color.get_red() + normal_color.get_green() + normal_color.get_blue()) / 3 < 0.5; + yellow.set_rgba(1.0, 1.0, 0.2); + double factor = light_theme ? 0.85 : 0.5; + yellow.set_red(normal_color.get_red() + factor * (yellow.get_red() - normal_color.get_red())); + yellow.set_green(normal_color.get_green() + factor * (yellow.get_green() - normal_color.get_green())); + yellow.set_blue(normal_color.get_blue() + factor * (yellow.get_blue() - normal_color.get_blue())); + red.set_rgba(1.0, 0.0, 0.0); + factor = light_theme ? 0.8 : 0.35; + red.set_red(normal_color.get_red() + factor * (red.get_red() - normal_color.get_red())); + red.set_green(normal_color.get_green() + factor * (red.get_green() - normal_color.get_green())); + red.set_blue(normal_color.get_blue() + factor * (red.get_blue() - normal_color.get_blue())); + green.set_rgba(0.0, 1.0, 0.0); + factor = light_theme ? 0.7 : 0.4; + green.set_red(normal_color.get_red() + factor * (green.get_red() - normal_color.get_red())); + green.set_green(normal_color.get_green() + factor * (green.get_green() - normal_color.get_green())); + green.set_blue(normal_color.get_blue() + factor * (green.get_blue() - normal_color.get_blue())); + transparent.set_rgba(1.0, 1.0, 1.0, 1.0); + + // Style gutter + renderer->set_alignment_mode(Gsv::GutterRendererAlignmentMode::GUTTER_RENDERER_ALIGNMENT_MODE_FIRST); + renderer->set_alignment(1.0, -1); + renderer->set_padding(3, -1); + + // Connect gutter renderer signals + renderer->signal_query_data().connect([this](const Gtk::TextIter &start, const Gtk::TextIter &end, Gsv::GutterRendererState state) { + update_gutter(find_coverage(start.get_line())); + }); + renderer->signal_query_activatable().connect([this](const Gtk::TextIter &, const Gdk::Rectangle &, GdkEvent *) { + return !line_coverage.empty(); + }); + + renderer->signal_query_tooltip().connect([this](const Gtk::TextIter &iter, const Gdk::Rectangle &area, int x, int y, const Glib::RefPtr &tooltip) { + if(const auto *entry = find_coverage(iter.get_line())) { + tooltip->set_text(update_tooltip(*entry)); + return true; + } + return false; + }); + + get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->insert(renderer.get(), -50); + renderer->set_visible(false); +} + +void Source::CoverageView::toggle_coverage() { + if(!line_coverage.empty()) { + update_coverage({}); + } + else if(get_coverage) { + update_coverage(get_coverage()); + } +} + +void Source::CoverageView::update_coverage(std::vector &&coverage) { + line_coverage = std::move(coverage); + unsigned long max_line_count = 0; + for(const auto &line : line_coverage) { + max_line_count = std::max(max_line_count, line.count); + } + + if(line_coverage.empty()) { + renderer->set_visible(false); + } + else { + int width, height; + renderer->measure(std::to_string(max_line_count), width, height); + renderer->set_visible(true); + renderer->set_size(width); + } + + renderer->queue_draw(); +} + +const Coverage::LineCoverage *Source::CoverageView::find_coverage(int line) { + if(line_coverage.empty()) { + return nullptr; + } + auto it = std::lower_bound(line_coverage.begin(), line_coverage.end(), line, [](const Coverage::LineCoverage &entry, int line) { + return entry.line < static_cast(line); + }); + if(it != line_coverage.end() && it->line == static_cast(line)) { + return &(*it); + } + + return nullptr; +} + +void Source::CoverageView::update_gutter(const Coverage::LineCoverage *line_coverage) { + if(!line_coverage) { + renderer->set_text(""); + renderer->set_background(renderer->transparent); + return; + } + + renderer->set_text(std::to_string(line_coverage->count)); + if(line_coverage->fully_covered()) { + renderer->set_background(renderer->green); + } + else if(line_coverage->partially_covered()) { + renderer->set_background(renderer->yellow); + } + else { + renderer->set_background(renderer->red); + } +} + +std::string Source::CoverageView::update_tooltip(const Coverage::LineCoverage &line_coverage) { + std::stringstream ss; + ss << "Line executed " << line_coverage.count << " times"; + if(line_coverage.count > 0 && line_coverage.has_unexecuted_statements) { + ss << " (with unexecuted statements)"; + } + + if(!line_coverage.branches.empty()) { + ss << ", with branches:"; + for(const auto &branch : line_coverage.branches) { + ss << "\n- " << (branch.is_fallthrough ? "Fall-through " : "") << (branch.is_throw ? "Exceptional " : "") << "Branch executed " << branch.count << " times"; + } + } + return ss.str(); +} diff --git a/src/source_coverage.hpp b/src/source_coverage.hpp new file mode 100644 index 0000000..76453c3 --- /dev/null +++ b/src/source_coverage.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "coverage.hpp" +#include "source_base.hpp" + +namespace Source { + class CoverageView : virtual public Source::BaseView { + + class Renderer : public Gsv::GutterRendererText { + public: + Renderer(); + + Gdk::RGBA yellow, red, green, transparent; + }; + + public: + CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language); + ~CoverageView() override; + + void configure() override; + + std::function()> get_coverage; + void toggle_coverage(); + + private: + std::unique_ptr renderer; + std::vector line_coverage; + + void update_coverage(std::vector &&coverage); + void update_gutter(const Coverage::LineCoverage *line_coverage); + std::string update_tooltip(const Coverage::LineCoverage &line_coverage); + + const Coverage::LineCoverage *find_coverage(int line); + }; +} // namespace Source diff --git a/src/window.cpp b/src/window.cpp index 718a746..bf6d4c1 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1376,6 +1376,12 @@ void Window::set_menu_actions() { } }); + menu.add_action("source_toggle_coverage", []() { + if(auto view = Notebook::get().get_current_view()) { + view->toggle_coverage(); + } + }); + menu.add_action("project_set_run_arguments", []() { auto project = Project::create(); auto run_arguments = project->get_run_arguments(); @@ -1804,6 +1810,7 @@ void Window::set_menu_actions() { menu.actions["source_implement_method"]->set_enabled(view && view->get_method); menu.actions["source_goto_next_diagnostic"]->set_enabled(view && view->goto_next_diagnostic); menu.actions["source_apply_fix_its"]->set_enabled(view && view->get_fix_its); + menu.actions["source_toggle_coverage"]->set_enabled(view && view->get_coverage); #ifdef JUCI_ENABLE_DEBUG Project::debug_activate_menu_items(); #endif