#include "source_base.h" #include "config.h" #include "git.h" #include "info.h" #include "terminal.h" #include Source::BaseView::BaseView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : Gsv::View(), file_path(file_path), language(language), status_diagnostics(0, 0, 0) { load(true); get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(0)); signal_focus_in_event().connect([this](GdkEventFocus *event) { if(this->last_write_time != static_cast(-1)) check_last_write_time(); return false; }); monitor_file(); if(language) { get_source_buffer()->set_language(language); auto language_id = language->get_id(); if(language_id == "chdr" || language_id == "cpphdr" || language_id == "c" || language_id == "cpp" || language_id == "objc" || language_id == "java" || language_id == "js" || language_id == "ts" || language_id == "proto" || language_id == "c-sharp" || language_id == "html" || language_id == "cuda" || language_id == "php" || language_id == "rust" || language_id == "swift" || language_id == "go" || language_id == "scala" || language_id == "opencl" || language_id == "json" || language_id == "css") is_bracket_language = true; } } Source::BaseView::~BaseView() { monitor_changed_connection.disconnect(); delayed_monitor_changed_connection.disconnect(); } bool Source::BaseView::load(bool not_undoable_action) { boost::system::error_code ec; last_write_time = boost::filesystem::last_write_time(file_path, ec); if(ec) last_write_time = static_cast(-1); disable_spellcheck = true; if(not_undoable_action) get_source_buffer()->begin_not_undoable_action(); class Guard { public: Source::BaseView *view; bool not_undoable_action; ~Guard() { if(not_undoable_action) view->get_source_buffer()->end_not_undoable_action(); view->disable_spellcheck = false; } }; Guard guard{this, not_undoable_action}; if(language) { std::ifstream input(file_path.string(), std::ofstream::binary); if(input) { std::stringstream ss; ss << input.rdbuf(); Glib::ustring ustr = ss.str(); bool valid = true; Glib::ustring::iterator iter; while(!ustr.validate(iter)) { auto next_char_iter = iter; next_char_iter++; ustr.replace(iter, next_char_iter, "?"); valid = false; } if(!valid) Terminal::get().print("Warning: " + file_path.string() + " is not a valid UTF-8 file. Saving might corrupt the file.\n"); if(get_buffer()->size() == 0) get_buffer()->insert_at_cursor(ustr); else replace_text(ustr.raw()); } else return false; } else { std::ifstream input(file_path.string(), std::ofstream::binary); if(input) { std::stringstream ss; ss << input.rdbuf(); Glib::ustring ustr = ss.str(); if(ustr.validate()) { if(get_buffer()->size() == 0) get_buffer()->insert_at_cursor(ustr); else replace_text(ustr.raw()); } else { Terminal::get().print("Error: " + file_path.string() + " is not a valid UTF-8 file.\n", true); return false; } } else return false; } get_buffer()->set_modified(false); return true; } void Source::BaseView::replace_text(const std::string &new_text) { get_buffer()->begin_user_action(); if(get_buffer()->size() == 0) { get_buffer()->insert_at_cursor(new_text); get_buffer()->end_user_action(); return; } else if(new_text.empty()) { get_buffer()->set_text(new_text); get_buffer()->end_user_action(); return; } auto iter = get_buffer()->get_insert()->get_iter(); int cursor_line_nr = iter.get_line(); int cursor_line_offset = iter.ends_line() ? std::numeric_limits::max() : iter.get_line_offset(); std::vector> new_lines; const char *line_start = new_text.c_str(); for(const char &chr : new_text) { if(chr == '\n') { new_lines.emplace_back(line_start, &chr + 1); line_start = &chr + 1; } } if(new_text.empty() || new_text.back() != '\n') new_lines.emplace_back(line_start, &new_text[new_text.size()]); try { auto hunks = Git::Repository::Diff::get_hunks(get_buffer()->get_text().raw(), new_text); for(auto it = hunks.rbegin(); it != hunks.rend(); ++it) { bool place_cursor = false; Gtk::TextIter start; if(it->old_lines.second != 0) { start = get_buffer()->get_iter_at_line(it->old_lines.first - 1); auto end = get_buffer()->get_iter_at_line(it->old_lines.first - 1 + it->old_lines.second); if(cursor_line_nr >= start.get_line() && cursor_line_nr < end.get_line()) { if(it->new_lines.second != 0) { place_cursor = true; int line_diff = cursor_line_nr - start.get_line(); cursor_line_nr += static_cast(0.5 + (static_cast(line_diff) / it->old_lines.second) * it->new_lines.second) - line_diff; } } get_buffer()->erase(start, end); start = get_buffer()->get_iter_at_line(it->old_lines.first - 1); } else start = get_buffer()->get_iter_at_line(it->old_lines.first); if(it->new_lines.second != 0) { get_buffer()->insert(start, new_lines[it->new_lines.first - 1].first, new_lines[it->new_lines.first - 1 + it->new_lines.second - 1].second); if(place_cursor) place_cursor_at_line_offset(cursor_line_nr, cursor_line_offset); } } } catch(...) { Terminal::get().print("Error: Could not replace text in buffer\n", true); } get_buffer()->end_user_action(); } void Source::BaseView::rename(const boost::filesystem::path &path) { file_path = path; if(update_status_file_path) update_status_file_path(this); if(update_tab_label) update_tab_label(this); } void Source::BaseView::monitor_file() { #ifdef __APPLE__ // TODO: Gio file monitor is bugged on MacOS class Recursive { public: static void f(BaseView *view, std::time_t last_write_time_) { view->delayed_monitor_changed_connection.disconnect(); view->delayed_monitor_changed_connection = Glib::signal_timeout().connect([view, last_write_time_]() { boost::system::error_code ec; auto last_write_time = boost::filesystem::last_write_time(view->file_path, ec); if(last_write_time != last_write_time_) view->check_last_write_time(last_write_time); Recursive::f(view, last_write_time); return false; }, 1000); } }; delayed_monitor_changed_connection.disconnect(); if(this->last_write_time != static_cast(-1)) Recursive::f(this, last_write_time); #else if(this->last_write_time != static_cast(-1)) { monitor = Gio::File::create_for_path(file_path.string())->monitor_file(Gio::FileMonitorFlags::FILE_MONITOR_NONE); monitor_changed_connection.disconnect(); monitor_changed_connection = monitor->signal_changed().connect([this](const Glib::RefPtr &file, const Glib::RefPtr &, Gio::FileMonitorEvent monitor_event) { if(monitor_event != Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { delayed_monitor_changed_connection.disconnect(); delayed_monitor_changed_connection = Glib::signal_timeout().connect([this]() { check_last_write_time(); return false; }, 500); } }); } #endif } void Source::BaseView::check_last_write_time(std::time_t last_write_time_) { if(this->last_write_time == static_cast(-1)) return; if(Config::get().source.auto_reload_changed_files && !get_buffer()->get_modified()) { boost::system::error_code ec; auto last_write_time = last_write_time_ != static_cast(-1) ? last_write_time_ : boost::filesystem::last_write_time(file_path, ec); if(!ec && last_write_time != this->last_write_time) { if(load()) return; } } else if(has_focus()) { boost::system::error_code ec; auto last_write_time = last_write_time_ != static_cast(-1) ? last_write_time_ : boost::filesystem::last_write_time(file_path, ec); if(!ec && last_write_time != this->last_write_time) Info::get().print("Caution: " + file_path.filename().string() + " was changed outside of juCi++"); } } Gtk::TextIter Source::BaseView::get_iter_at_line_pos(int line, int pos) { return get_iter_at_line_index(line, pos); } Gtk::TextIter Source::BaseView::get_iter_at_line_offset(int line, int offset) { line = std::min(line, get_buffer()->get_line_count() - 1); if(line < 0) line = 0; auto iter = get_iter_at_line_end(line); offset = std::min(offset, iter.get_line_offset()); if(offset < 0) offset = 0; return get_buffer()->get_iter_at_line_offset(line, offset); } Gtk::TextIter Source::BaseView::get_iter_at_line_index(int line, int index) { line = std::min(line, get_buffer()->get_line_count() - 1); if(line < 0) line = 0; auto iter = get_iter_at_line_end(line); index = std::min(index, iter.get_line_index()); if(index < 0) index = 0; return get_buffer()->get_iter_at_line_index(line, index); } Gtk::TextIter Source::BaseView::get_iter_at_line_end(int line_nr) { if(line_nr >= get_buffer()->get_line_count()) return get_buffer()->end(); else if(line_nr + 1 < get_buffer()->get_line_count()) { auto iter = get_buffer()->get_iter_at_line(line_nr + 1); iter.backward_char(); if(!iter.ends_line()) // for CR+LF iter.backward_char(); return iter; } else { auto iter = get_buffer()->get_iter_at_line(line_nr); while(!iter.ends_line() && iter.forward_char()) { } return iter; } } Gtk::TextIter Source::BaseView::get_iter_for_dialog() { auto iter = get_buffer()->get_insert()->get_iter(); Gdk::Rectangle visible_rect; get_visible_rect(visible_rect); Gdk::Rectangle iter_rect; get_iter_location(iter, iter_rect); iter_rect.set_width(1); if(iter.get_line_offset() >= 80) { get_iter_at_location(iter, visible_rect.get_x(), iter_rect.get_y()); get_iter_location(iter, iter_rect); } if(!visible_rect.intersects(iter_rect)) get_iter_at_location(iter, visible_rect.get_x(), visible_rect.get_y() + visible_rect.get_height() / 3); return iter; } void Source::BaseView::place_cursor_at_line_pos(int line, int pos) { get_buffer()->place_cursor(get_iter_at_line_pos(line, pos)); } void Source::BaseView::place_cursor_at_line_offset(int line, int offset) { get_buffer()->place_cursor(get_iter_at_line_offset(line, offset)); } void Source::BaseView::place_cursor_at_line_index(int line, int index) { get_buffer()->place_cursor(get_iter_at_line_index(line, index)); } Gtk::TextIter Source::BaseView::get_smart_home_iter(const Gtk::TextIter &iter) { auto start_line_iter = get_buffer()->get_iter_at_line(iter.get_line()); auto start_sentence_iter = start_line_iter; while(!start_sentence_iter.ends_line() && (*start_sentence_iter == ' ' || *start_sentence_iter == '\t') && start_sentence_iter.forward_char()) { } if(iter > start_sentence_iter || iter == start_line_iter) return start_sentence_iter; else return start_line_iter; } Gtk::TextIter Source::BaseView::get_smart_end_iter(const Gtk::TextIter &iter) { auto end_line_iter = get_iter_at_line_end(iter.get_line()); auto end_sentence_iter = end_line_iter; while(!end_sentence_iter.starts_line() && (*end_sentence_iter == ' ' || *end_sentence_iter == '\t' || end_sentence_iter.ends_line()) && end_sentence_iter.backward_char()) { } if(!end_sentence_iter.ends_line() && *end_sentence_iter != ' ' && *end_sentence_iter != '\t') end_sentence_iter.forward_char(); if(iter == end_line_iter) return end_sentence_iter; else return end_line_iter; } std::string Source::BaseView::get_line(const Gtk::TextIter &iter) { auto line_start_it = get_buffer()->get_iter_at_line(iter.get_line()); auto line_end_it = get_iter_at_line_end(iter.get_line()); std::string line(get_buffer()->get_text(line_start_it, line_end_it)); return line; } std::string Source::BaseView::get_line(const Glib::RefPtr &mark) { return get_line(mark->get_iter()); } std::string Source::BaseView::get_line(int line_nr) { return get_line(get_buffer()->get_iter_at_line(line_nr)); } std::string Source::BaseView::get_line() { return get_line(get_buffer()->get_insert()); } std::string Source::BaseView::get_line_before(const Gtk::TextIter &iter) { auto line_it = get_buffer()->get_iter_at_line(iter.get_line()); std::string line(get_buffer()->get_text(line_it, iter)); return line; } std::string Source::BaseView::get_line_before(const Glib::RefPtr &mark) { return get_line_before(mark->get_iter()); } std::string Source::BaseView::get_line_before() { return get_line_before(get_buffer()->get_insert()); } Gtk::TextIter Source::BaseView::get_tabs_end_iter(const Gtk::TextIter &iter) { return get_tabs_end_iter(iter.get_line()); } Gtk::TextIter Source::BaseView::get_tabs_end_iter(const Glib::RefPtr &mark) { return get_tabs_end_iter(mark->get_iter()); } Gtk::TextIter Source::BaseView::get_tabs_end_iter(int line_nr) { auto sentence_iter = get_buffer()->get_iter_at_line(line_nr); while((*sentence_iter == ' ' || *sentence_iter == '\t') && !sentence_iter.ends_line() && sentence_iter.forward_char()) { } return sentence_iter; } Gtk::TextIter Source::BaseView::get_tabs_end_iter() { return get_tabs_end_iter(get_buffer()->get_insert()); } void Source::BaseView::place_cursor_at_next_diagnostic() { auto insert_offset = get_buffer()->get_insert()->get_iter().get_offset(); for(auto offset : diagnostic_offsets) { if(offset > insert_offset) { get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(offset)); scroll_to(get_buffer()->get_insert(), 0.0, 1.0, 0.5); return; } } if(diagnostic_offsets.size() == 0) Info::get().print("No diagnostics found in current buffer"); else { auto iter = get_buffer()->get_iter_at_offset(*diagnostic_offsets.begin()); get_buffer()->place_cursor(iter); scroll_to(get_buffer()->get_insert(), 0.0, 1.0, 0.5); } }