#include "project.h" #include "config.h" #include "terminal.h" #include "filesystem.h" #include "directories.h" #include #include "menu.h" #include "notebook.h" #include "selection_dialog.h" #ifdef JUCI_ENABLE_DEBUG #include "debug_lldb.h" #endif #include "info.h" boost::filesystem::path Project::debug_last_stop_file_path; std::unordered_map Project::run_arguments; std::unordered_map Project::debug_run_arguments; std::atomic Project::compiling(false); std::atomic Project::debugging(false); std::pair > Project::debug_stop; std::string Project::debug_status; std::unique_ptr Project::current; #ifdef JUCI_ENABLE_DEBUG std::unordered_map Project::Clang::debug_options; #endif Gtk::Label &Project::debug_status_label() { static Gtk::Label label; return label; } void Project::save_files(const boost::filesystem::path &path) { for(size_t c=0;cget_buffer()->get_modified()) { if(filesystem::file_in_path(view->file_path, path)) Notebook::get().save(c); } } } void Project::on_save(size_t index) { auto view=Notebook::get().get_view(index); if(!view) return; boost::filesystem::path build_path; if(view->language && view->language->get_id()=="cmake") { if(view->file_path.filename()=="CMakeLists.txt") build_path=view->file_path; else build_path=filesystem::find_file_in_path_parents("CMakeLists.txt", view->file_path.parent_path()); } else if(view->language && view->language->get_id()=="meson") { if(view->file_path.filename()=="meson.build") build_path=view->file_path; else build_path=filesystem::find_file_in_path_parents("meson.build", view->file_path.parent_path()); } if(!build_path.empty()) { auto build=Build::create(build_path); if(dynamic_cast(build.get()) || dynamic_cast(build.get())) { build->update_default(true); if(boost::filesystem::exists(build->get_debug_path())) build->update_debug(true); for(size_t c=0;c(source_view)) { if(filesystem::file_in_path(source_clang_view->file_path, build->project_path)) source_clang_view->full_reparse_needed=true; } } } } } void Project::debug_update_status(const std::string &new_debug_status) { debug_status=new_debug_status; if(debug_status.empty()) debug_status_label().set_text(""); else debug_status_label().set_text(debug_status); debug_activate_menu_items(); } void Project::debug_activate_menu_items() { auto &menu=Menu::get(); auto view=Notebook::get().get_current_view(); menu.actions["debug_stop"]->set_enabled(!debug_status.empty()); menu.actions["debug_kill"]->set_enabled(!debug_status.empty()); menu.actions["debug_step_over"]->set_enabled(!debug_status.empty()); menu.actions["debug_step_into"]->set_enabled(!debug_status.empty()); menu.actions["debug_step_out"]->set_enabled(!debug_status.empty()); menu.actions["debug_backtrace"]->set_enabled(!debug_status.empty()); menu.actions["debug_show_variables"]->set_enabled(!debug_status.empty()); menu.actions["debug_run_command"]->set_enabled(!debug_status.empty()); menu.actions["debug_toggle_breakpoint"]->set_enabled(view && view->toggle_breakpoint); menu.actions["debug_goto_stop"]->set_enabled(!debug_status.empty()); } void Project::debug_update_stop() { if(!debug_last_stop_file_path.empty()) { for(size_t c=0;cfile_path==debug_last_stop_file_path) { view->get_source_buffer()->remove_source_marks(view->get_buffer()->begin(), view->get_buffer()->end(), "debug_stop"); view->get_source_buffer()->remove_source_marks(view->get_buffer()->begin(), view->get_buffer()->end(), "debug_breakpoint_and_stop"); break; } } } //Add debug stop source mark debug_last_stop_file_path.clear(); for(size_t c=0;cfile_path==debug_stop.first) { if(debug_stop.second.firstget_buffer()->get_line_count()) { auto iter=view->get_buffer()->get_iter_at_line(debug_stop.second.first); view->get_source_buffer()->create_source_mark("debug_stop", iter); if(view->get_source_buffer()->get_source_marks_at_iter(iter, "debug_breakpoint").size()>0) view->get_source_buffer()->create_source_mark("debug_breakpoint_and_stop", iter); debug_last_stop_file_path=debug_stop.first; } break; } } } std::unique_ptr Project::create() { std::unique_ptr build; if(auto view=Notebook::get().get_current_view()) { build=Build::create(view->file_path); if(view->language) { auto language_id=view->language->get_id(); if(language_id=="markdown") return std::unique_ptr(new Project::Markdown(std::move(build))); if(language_id=="python") return std::unique_ptr(new Project::Python(std::move(build))); if(language_id=="js") return std::unique_ptr(new Project::JavaScript(std::move(build))); if(language_id=="html") return std::unique_ptr(new Project::HTML(std::move(build))); } } else build=Build::create(Directories::get().path); if(dynamic_cast(build.get()) || dynamic_cast(build.get())) return std::unique_ptr(new Project::Clang(std::move(build))); else return std::unique_ptr(new Project::Base(std::move(build))); } std::pair Project::Base::get_run_arguments() { Info::get().print("Could not find a supported project"); return {"", ""}; } void Project::Base::compile() { Info::get().print("Could not find a supported project"); } void Project::Base::compile_and_run() { Info::get().print("Could not find a supported project"); } void Project::Base::recreate_build() { Info::get().print("Could not find a supported project"); } std::pair Project::Base::debug_get_run_arguments() { Info::get().print("Could not find a supported project"); return {"", ""}; } void Project::Base::debug_start() { Info::get().print("Could not find a supported project"); } #ifdef JUCI_ENABLE_DEBUG Project::Clang::DebugOptions::DebugOptions() : Base::DebugOptions() { remote_enabled.set_active(false); remote_enabled.set_label("Enabled"); remote_enabled.signal_clicked().connect([this] { remote_host.set_sensitive(remote_enabled.get_active()); }); remote_host.set_sensitive(false); remote_host.set_placeholder_text("host:port"); remote_host.signal_activate().connect([this] { set_visible(false); }); auto remote_vbox=Gtk::manage(new Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL)); remote_vbox->pack_start(remote_enabled, true, true); remote_vbox->pack_end(remote_host, true, true); auto remote_frame=Gtk::manage(new Gtk::Frame()); remote_frame->set_label("Remote Debugging"); remote_frame->add(*remote_vbox); vbox.pack_end(*remote_frame, true, true); show_all(); set_visible(false); } #endif std::pair Project::Clang::get_run_arguments() { auto build_path=build->get_default_path(); if(build_path.empty()) return {"", ""}; auto project_path=build->project_path.string(); auto run_arguments_it=run_arguments.find(project_path); std::string arguments; if(run_arguments_it!=run_arguments.end()) arguments=run_arguments_it->second; if(arguments.empty()) { auto view=Notebook::get().get_current_view(); auto executable=build->get_executable(view?view->file_path:Directories::get().path).string(); if(!executable.empty()) arguments=filesystem::escape_argument(executable); else arguments=filesystem::escape_argument(build->get_default_path().string()); } return {project_path, arguments}; } void Project::Clang::compile() { auto default_build_path=build->get_default_path(); if(default_build_path.empty() || !build->update_default()) return; if(Config::get().project.clear_terminal_on_compile) Terminal::get().clear(); compiling=true; Terminal::get().print("Compiling project "+build->project_path.string()+"\n"); std::string compile_command; if(dynamic_cast(build.get())) compile_command=Config::get().project.cmake.compile_command; else if(dynamic_cast(build.get())) compile_command=Config::get().project.meson.compile_command; Terminal::get().async_process(compile_command, default_build_path, [this](int exit_status) { compiling=false; }); } void Project::Clang::compile_and_run() { auto default_build_path=build->get_default_path(); if(default_build_path.empty() || !build->update_default()) return; auto project_path=build->project_path; auto run_arguments_it=run_arguments.find(project_path.string()); std::string arguments; if(run_arguments_it!=run_arguments.end()) arguments=run_arguments_it->second; if(arguments.empty()) { auto view=Notebook::get().get_current_view(); arguments=build->get_executable(view?view->file_path:Directories::get().path).string(); if(arguments.empty()) { Terminal::get().print("Warning: could not find executable.\n"); Terminal::get().print("Solution: either use Project Set Run Arguments, or open a source file within a directory where an executable is defined.\n", true); return; } arguments=filesystem::escape_argument(arguments); } if(Config::get().project.clear_terminal_on_compile) Terminal::get().clear(); compiling=true; Terminal::get().print("Compiling and running "+arguments+"\n"); std::string compile_command; if(dynamic_cast(build.get())) compile_command=Config::get().project.cmake.compile_command; else if(dynamic_cast(build.get())) compile_command=Config::get().project.meson.compile_command; Terminal::get().async_process(compile_command, default_build_path, [this, arguments, project_path](int exit_status){ compiling=false; if(exit_status==EXIT_SUCCESS) { Terminal::get().async_process(arguments, project_path, [this, arguments](int exit_status){ Terminal::get().async_print(arguments+" returned: "+std::to_string(exit_status)+'\n'); }); } }); } void Project::Clang::recreate_build() { auto default_build_path=build->get_default_path(); if(default_build_path.empty()) return; auto debug_build_path=build->get_debug_path(); bool has_default_build=boost::filesystem::exists(default_build_path); bool has_debug_build=!debug_build_path.empty() && boost::filesystem::exists(debug_build_path); if(has_default_build || has_debug_build) { Gtk::MessageDialog dialog(*static_cast(Notebook::get().get_toplevel()), "Recreate Build", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); dialog.set_default_response(Gtk::RESPONSE_NO); std::string message="Are you sure you want to recreate "; if(has_default_build) message+=default_build_path.string(); if(has_debug_build) { if(has_default_build) message+=" and "; message+=debug_build_path.string(); } dialog.set_secondary_text(message+"?"); if(dialog.run()!=Gtk::RESPONSE_YES) return; try { if(has_default_build) boost::filesystem::remove_all(default_build_path); if(has_debug_build) boost::filesystem::remove_all(debug_build_path); } catch(const std::exception &e) { Terminal::get().print(std::string("Error: could remove build: ")+e.what()+"\n", true); return; } } build->update_default(true); if(has_debug_build) build->update_debug(true); for(size_t c=0;c(source_view)) { if(filesystem::file_in_path(source_clang_view->file_path, build->project_path)) source_clang_view->full_reparse_needed=true; } } if(auto view=Notebook::get().get_current_view()) { if(view->full_reparse_needed) view->full_reparse(); } } #ifdef JUCI_ENABLE_DEBUG std::pair Project::Clang::debug_get_run_arguments() { auto debug_build_path=build->get_debug_path(); auto default_build_path=build->get_default_path(); if(debug_build_path.empty() || default_build_path.empty()) return {"", ""}; auto project_path=build->project_path.string(); auto run_arguments_it=debug_run_arguments.find(project_path); std::string arguments; if(run_arguments_it!=debug_run_arguments.end()) arguments=run_arguments_it->second; if(arguments.empty()) { auto view=Notebook::get().get_current_view(); auto executable=build->get_executable(view?view->file_path:Directories::get().path).string(); if(!executable.empty()) { size_t pos=executable.find(default_build_path.string()); if(pos!=std::string::npos) executable.replace(pos, default_build_path.string().size(), debug_build_path.string()); arguments=filesystem::escape_argument(executable); } else arguments=filesystem::escape_argument(build->get_debug_path().string()); } return {project_path, arguments}; } Gtk::Popover *Project::Clang::debug_get_options() { if(!build->project_path.empty()) return &debug_options[build->project_path.string()]; return nullptr; } void Project::Clang::debug_start() { auto debug_build_path=build->get_debug_path(); auto default_build_path=build->get_default_path(); if(debug_build_path.empty() || !build->update_debug() || default_build_path.empty()) return; auto project_path=std::make_shared(build->project_path); auto run_arguments_it=debug_run_arguments.find(project_path->string()); auto run_arguments=std::make_shared(); if(run_arguments_it!=debug_run_arguments.end()) *run_arguments=run_arguments_it->second; if(run_arguments->empty()) { auto view=Notebook::get().get_current_view(); *run_arguments=build->get_executable(view?view->file_path:Directories::get().path).string(); if(run_arguments->empty()) { Terminal::get().print("Warning: could not find executable.\n"); Terminal::get().print("Solution: either use Debug Set Run Arguments, or open a source file within a directory where an executable is defined.\n", true); return; } size_t pos=run_arguments->find(default_build_path.string()); if(pos!=std::string::npos) run_arguments->replace(pos, default_build_path.string().size(), debug_build_path.string()); *run_arguments=filesystem::escape_argument(*run_arguments); } if(Config::get().project.clear_terminal_on_compile) Terminal::get().clear(); debugging=true; Terminal::get().print("Compiling and debugging "+*run_arguments+"\n"); std::string compile_command; if(dynamic_cast(build.get())) compile_command=Config::get().project.cmake.compile_command; else if(dynamic_cast(build.get())) compile_command=Config::get().project.meson.compile_command; Terminal::get().async_process(compile_command, debug_build_path, [this, run_arguments, project_path](int exit_status){ if(exit_status!=EXIT_SUCCESS) debugging=false; else { dispatcher.post([this, run_arguments, project_path] { std::vector > breakpoints; for(size_t c=0;cfile_path, *project_path)) { auto iter=view->get_buffer()->begin(); if(view->get_source_buffer()->get_source_marks_at_iter(iter, "debug_breakpoint").size()>0) breakpoints.emplace_back(view->file_path, iter.get_line()+1); while(view->get_source_buffer()->forward_iter_to_source_mark(iter, "debug_breakpoint")) breakpoints.emplace_back(view->file_path, iter.get_line()+1); } } std::string remote_host; auto options_it=debug_options.find(project_path->string()); if(options_it!=debug_options.end() && options_it->second.remote_enabled.get_active()) remote_host=options_it->second.remote_host.get_text(); Debug::LLDB::get().start(*run_arguments, *project_path, breakpoints, [this, run_arguments](int exit_status){ debugging=false; Terminal::get().async_print(*run_arguments+" returned: "+std::to_string(exit_status)+'\n'); }, [this](const std::string &status) { dispatcher.post([this, status] { debug_update_status(status); }); }, [this](const boost::filesystem::path &file_path, int line_nr, int line_index) { dispatcher.post([this, file_path, line_nr, line_index] { Project::debug_stop.first=file_path; Project::debug_stop.second.first=line_nr-1; Project::debug_stop.second.second=line_index-1; debug_update_stop(); if(auto view=Notebook::get().get_current_view()) view->get_buffer()->place_cursor(view->get_buffer()->get_insert()->get_iter()); }); }, remote_host); }); } }); } void Project::Clang::debug_continue() { Debug::LLDB::get().continue_debug(); } void Project::Clang::debug_stop() { if(debugging) Debug::LLDB::get().stop(); } void Project::Clang::debug_kill() { if(debugging) Debug::LLDB::get().kill(); } void Project::Clang::debug_step_over() { if(debugging) Debug::LLDB::get().step_over(); } void Project::Clang::debug_step_into() { if(debugging) Debug::LLDB::get().step_into(); } void Project::Clang::debug_step_out() { if(debugging) Debug::LLDB::get().step_out(); } void Project::Clang::debug_backtrace() { if(debugging) { auto view=Notebook::get().get_current_view(); auto backtrace=Debug::LLDB::get().get_backtrace(); if(view) { auto iter=view->get_iter_for_dialog(); SelectionDialog::create(view, view->get_buffer()->create_mark(iter), true, true); } else SelectionDialog::create(true, true); auto rows=std::make_shared >(); if(backtrace.size()==0) { Info::get().print("No backtrace found"); return; } bool cursor_set=false; for(auto &frame: backtrace) { std::string row=""+frame.module_filename+""; //Shorten frame.function_name if it is too long if(frame.function_name.size()>120) { frame.function_name=frame.function_name.substr(0, 58)+"...."+frame.function_name.substr(frame.function_name.size()-58); } if(frame.file_path.empty()) row+=" - "+Glib::Markup::escape_text(frame.function_name); else { auto file_path=frame.file_path.filename().string(); row+=":"+Glib::Markup::escape_text(file_path)+":"+std::to_string(frame.line_nr)+" - "+Glib::Markup::escape_text(frame.function_name); } (*rows)[row]=frame; SelectionDialog::get()->add_row(row); if(!cursor_set && view && frame.file_path==view->file_path) { SelectionDialog::get()->set_cursor_at_last_row(); cursor_set=true; } } SelectionDialog::get()->on_select=[this, rows](const std::string& selected, bool hide_window) { auto frame=rows->at(selected); if(!frame.file_path.empty()) { Notebook::get().open(frame.file_path); if(auto view=Notebook::get().get_current_view()) { Debug::LLDB::get().select_frame(frame.index); view->place_cursor_at_line_index(frame.line_nr-1, frame.line_index-1); view->scroll_to_cursor_delayed(view, true, true); } } }; if(view) view->hide_tooltips(); SelectionDialog::get()->show(); } } void Project::Clang::debug_show_variables() { if(debugging) { auto view=Notebook::get().get_current_view(); auto variables=Debug::LLDB::get().get_variables(); Gtk::TextIter iter; if(view) { iter=view->get_iter_for_dialog(); SelectionDialog::create(view, view->get_buffer()->create_mark(iter), true, true); } else SelectionDialog::create(true, true); auto rows=std::make_shared >(); if(variables.size()==0) { Info::get().print("No variables found"); return; } for(auto &variable: variables) { std::string row="#"+std::to_string(variable.thread_index_id)+":#"+std::to_string(variable.frame_index)+":"+variable.file_path.filename().string()+":"+std::to_string(variable.line_nr)+" - "+Glib::Markup::escape_text(variable.name)+""; (*rows)[row]=variable; SelectionDialog::get()->add_row(row); } SelectionDialog::get()->on_select=[this, rows](const std::string& selected, bool hide_window) { auto variable=rows->at(selected); Debug::LLDB::get().select_frame(variable.frame_index, variable.thread_index_id); if(!variable.file_path.empty()) { Notebook::get().open(variable.file_path); if(auto view=Notebook::get().get_current_view()) { view->place_cursor_at_line_index(variable.line_nr-1, variable.line_index-1); view->scroll_to_cursor_delayed(view, true, true); } } if(!variable.declaration_found) Info::get().print("Debugger did not find declaration for the variable: "+variable.name); }; SelectionDialog::get()->on_hide=[this]() { debug_variable_tooltips.hide(); debug_variable_tooltips.clear(); }; SelectionDialog::get()->on_changed=[this, rows, view, iter](const std::string &selected) { if(selected.empty()) { debug_variable_tooltips.hide(); return; } debug_variable_tooltips.clear(); auto create_tooltip_buffer=[this, rows, view, selected]() { auto variable=rows->at(selected); auto tooltip_buffer=view?Gtk::TextBuffer::create(view->get_buffer()->get_tag_table()):Gtk::TextBuffer::create(); Glib::ustring value=variable.value; if(!value.empty()) { Glib::ustring::iterator iter; while(!value.validate(iter)) { auto next_char_iter=iter; next_char_iter++; value.replace(iter, next_char_iter, "?"); } if(view) tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), value.substr(0, value.size()-1), "def:note"); else tooltip_buffer->insert(tooltip_buffer->get_insert()->get_iter(), value.substr(0, value.size()-1)); } return tooltip_buffer; }; if(view) debug_variable_tooltips.emplace_back(create_tooltip_buffer, view, view->get_buffer()->create_mark(iter), view->get_buffer()->create_mark(iter)); else debug_variable_tooltips.emplace_back(create_tooltip_buffer); debug_variable_tooltips.show(true); }; if(view) view->hide_tooltips(); SelectionDialog::get()->show(); } } void Project::Clang::debug_run_command(const std::string &command) { if(debugging) { auto command_return=Debug::LLDB::get().run_command(command); Terminal::get().async_print(command_return.first); Terminal::get().async_print(command_return.second, true); } } void Project::Clang::debug_add_breakpoint(const boost::filesystem::path &file_path, int line_nr) { Debug::LLDB::get().add_breakpoint(file_path, line_nr); } void Project::Clang::debug_remove_breakpoint(const boost::filesystem::path &file_path, int line_nr, int line_count) { Debug::LLDB::get().remove_breakpoint(file_path, line_nr, line_count); } bool Project::Clang::debug_is_running() { return Debug::LLDB::get().is_running(); } void Project::Clang::debug_write(const std::string &buffer) { Debug::LLDB::get().write(buffer); } void Project::Clang::debug_cancel() { Debug::LLDB::get().cancel(); } #endif Project::Markdown::~Markdown() { if(!last_temp_path.empty()) { boost::filesystem::remove(last_temp_path); last_temp_path=boost::filesystem::path(); } } void Project::Markdown::compile_and_run() { if(!last_temp_path.empty()) { boost::filesystem::remove(last_temp_path); last_temp_path=boost::filesystem::path(); } std::stringstream stdin_stream, stdout_stream; auto exit_status=Terminal::get().process(stdin_stream, stdout_stream, "command -v grip"); if(exit_status==0) { auto command="grip -b "+filesystem::escape_argument(Notebook::get().get_current_view()->file_path.string()); Terminal::get().print("Running: "+command+" in a quiet background process\n"); Terminal::get().async_process(command, "", nullptr, true); } else Terminal::get().print("Warning: install grip to preview Markdown files\n"); } void Project::Python::compile_and_run() { auto command="PYTHONUNBUFFERED=1 python "+filesystem::escape_argument(Notebook::get().get_current_view()->file_path.string()); Terminal::get().print("Running "+command+"\n"); Terminal::get().async_process(command, Notebook::get().get_current_view()->file_path.parent_path(), [command](int exit_status) { Terminal::get().async_print(command+" returned: "+std::to_string(exit_status)+'\n'); }); } void Project::JavaScript::compile_and_run() { auto command="node --harmony "+filesystem::escape_argument(Notebook::get().get_current_view()->file_path.string()); Terminal::get().print("Running "+command+"\n"); Terminal::get().async_process(command, Notebook::get().get_current_view()->file_path.parent_path(), [command](int exit_status) { Terminal::get().async_print(command+" returned: "+std::to_string(exit_status)+'\n'); }); } void Project::HTML::compile_and_run() { auto uri=Notebook::get().get_current_view()->file_path.string(); #ifdef __APPLE__ Terminal::get().process("open "+filesystem::escape_argument(uri)); #else #ifdef __linux uri="file://"+uri; #endif GError* error=nullptr; gtk_show_uri(nullptr, uri.c_str(), GDK_CURRENT_TIME, &error); g_clear_error(&error); #endif }