<commit_before>#include "source.h"
#include "sourcefile.h"
#include <boost/property_tree/json_parser.hpp>
#include <boost/timer/timer.hpp>
#include "logging.h"
#include <algorithm>
#include "singletons.h"
#include <gtksourceview/gtksource.h>
#include <boost/lexical_cast.hpp>
#include <iostream>

using namespace std; //TODO: remove

namespace sigc {
#ifndef SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
  template <typename Functor>
  struct functor_trait<Functor, false> {
    typedef decltype (::sigc::mem_fun(std::declval<Functor&>(),
                                      &Functor::operator())) _intermediate;
    typedef typename _intermediate::result_type result_type;
    typedef Functor functor_type;
  };
#else
  SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
#endif
}

Glib::RefPtr<Gsv::Language> Source::guess_language(const boost::filesystem::path &file_path) {
  auto language_manager=Gsv::LanguageManager::get_default();
  bool result_uncertain = false;
  auto content_type = Gio::content_type_guess(file_path.string(), NULL, 0, result_uncertain);
  if(result_uncertain) {
    content_type.clear();
  }
  auto language=language_manager->guess_language(file_path.string(), content_type);
  if(!language) {
    auto filename=file_path.filename().string();
    if(filename=="CMakeLists.txt")
      language=language_manager->get_language("cmake");
    else if(filename=="Makefile")
      language=language_manager->get_language("makefile");
  }
  return language;
}

//////////////
//// View ////
//////////////
AspellConfig* Source::View::spellcheck_config=NULL;

Source::View::View(const boost::filesystem::path &file_path, Glib::RefPtr<Gsv::Language> language): file_path(file_path) {
  set_smart_home_end(Gsv::SMART_HOME_END_BEFORE);
  
  get_source_buffer()->begin_not_undoable_action();
  if(language) {
    if(juci::filesystem::read_non_utf8(file_path, get_buffer())==-1)
      Singleton::terminal()->print("Warning: "+file_path.string()+" is not a valid UTF-8 file. Saving might corrupt the file.\n");
  }
  else {
    if(juci::filesystem::read(file_path, get_buffer())==-1)
      Singleton::terminal()->print("Error: "+file_path.string()+" is not a valid UTF-8 file.\n");
  }
  get_source_buffer()->end_not_undoable_action();
  
  get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(0)); 
  
  search_settings = gtk_source_search_settings_new();
  gtk_source_search_settings_set_wrap_around(search_settings, true);
  search_context = gtk_source_search_context_new(get_source_buffer()->gobj(), search_settings);
  gtk_source_search_context_set_highlight(search_context, true);
  //TODO: why does this not work?: Might be best to use the styles from sourceview. These has to be read from file, search-matches got style "search-match"
  //TODO: in header if trying again: GtkSourceStyle* search_match_style;
  //TODO: We can drop this, only work on newer versions of gtksourceview.
  //search_match_style=(GtkSourceStyle*)g_object_new(GTK_SOURCE_TYPE_STYLE, "background-set", 1, "background", "#00FF00", NULL);
  //gtk_source_search_context_set_match_style(search_context, search_match_style);
  
  //TODO: either use lambda if possible or create a gtkmm wrapper around search_context (including search_settings):
  //TODO: (gtkmm's Gtk::Object has connect_property_changed, so subclassing this might be an idea)
  g_signal_connect(search_context, "notify::occurrences-count", G_CALLBACK(search_occurrences_updated), this);
  
  //TODO: Move this to notebook? Might take up too much memory doing this for every tab.
  auto style_scheme_manager=Gsv::StyleSchemeManager::get_default();
  style_scheme_manager->prepend_search_path(Singleton::style_dir());

  if(Singleton::Config::source()->style.size()>0) {
    auto scheme = style_scheme_manager->get_scheme(Singleton::Config::source()->style);

    if(scheme)
      get_source_buffer()->set_style_scheme(scheme);
    else
      Singleton::terminal()->print("Error: Could not find gtksourceview style: "+Singleton::Config::source()->style+'\n');
  }
  
  if(Singleton::Config::source()->wrap_lines)
    set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
  property_highlight_current_line() = Singleton::Config::source()->highlight_current_line;
  property_show_line_numbers() = Singleton::Config::source()->show_line_numbers;
  if(Singleton::Config::source()->font.size()>0)
    override_font(Pango::FontDescription(Singleton::Config::source()->font));
  
  //Create tags for diagnostic warnings and errors:
  auto scheme = get_source_buffer()->get_style_scheme();
  auto tag_table=get_buffer()->get_tag_table();
  auto style=scheme->get_style("def:warning");
  auto diagnostic_tag=get_source_buffer()->create_tag("def:warning");
  auto diagnostic_tag_underline=get_source_buffer()->create_tag("def:warning_underline");
  if(style && (style->property_foreground_set() || style->property_background_set())) {
    Glib::ustring warning_property;
    if(style->property_foreground_set()) {
      warning_property=style->property_foreground().get_value();
      diagnostic_tag->property_foreground() = warning_property;
    }
    else if(style->property_background_set())
      warning_property=style->property_background().get_value();

    diagnostic_tag_underline->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    auto tag_class=G_OBJECT_GET_CLASS(diagnostic_tag_underline->gobj()); //For older GTK+ 3 versions:
    auto param_spec=g_object_class_find_property(tag_class, "underline-rgba");
    if(param_spec!=NULL) {
      diagnostic_tag_underline->set_property("underline-rgba", Gdk::RGBA(warning_property));
    }
  }
  style=scheme->get_style("def:error");
  diagnostic_tag=get_source_buffer()->create_tag("def:error");
  diagnostic_tag_underline=get_source_buffer()->create_tag("def:error_underline");
  if(style && (style->property_foreground_set() || style->property_background_set())) {
    Glib::ustring error_property;
    if(style->property_foreground_set()) {
      error_property=style->property_foreground().get_value();
      diagnostic_tag->property_foreground() = error_property;
    }
    else if(style->property_background_set())
      error_property=style->property_background().get_value();
    
    diagnostic_tag_underline->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    auto tag_class=G_OBJECT_GET_CLASS(diagnostic_tag_underline->gobj()); //For older GTK+ 3 versions:
    auto param_spec=g_object_class_find_property(tag_class, "underline-rgba");
    if(param_spec!=NULL) {
      diagnostic_tag_underline->set_property("underline-rgba", Gdk::RGBA(error_property));
    }
  }
  //TODO: clear tag_class and param_spec?
  
  //Add tooltip foreground and background
  style = scheme->get_style("def:note");
  auto note_tag=get_source_buffer()->create_tag("def:note_background");
  if(style->property_background_set()) {
    note_tag->property_background()=style->property_background();
  }
  note_tag=get_source_buffer()->create_tag("def:note");
  if(style->property_foreground_set()) {
    note_tag->property_foreground()=style->property_foreground();
  }
  
  tab_char=Singleton::Config::source()->default_tab_char;
  tab_size=Singleton::Config::source()->default_tab_size;
  if(Singleton::Config::source()->auto_tab_char_and_size) {
    auto tab_char_and_size=find_tab_char_and_size();
    if(tab_char_and_size.first!=0) {
      if(tab_char!=tab_char_and_size.first || tab_size!=tab_char_and_size.second) {
        std::string tab_str;
        if(tab_char_and_size.first==' ')
          tab_str="<space>";
        else
          tab_str="<tab>";
        Singleton::terminal()->print("Tab char and size for file "+file_path.string()+" set to: "+tab_str+", "+boost::lexical_cast<std::string>(tab_char_and_size.second)+".\n");
      }
      
      tab_char=tab_char_and_size.first;
      tab_size=tab_char_and_size.second;
    }
  }
  for(unsigned c=0;c<tab_size;c++)
    tab+=tab_char;
  
  tabs_regex=std::regex(std::string("^(")+tab_char+"*)(.*)$");
  
  if(spellcheck_config==NULL) {
    spellcheck_config=new_aspell_config();
    if(Singleton::Config::source()->spellcheck_language.size()>0)
      aspell_config_replace(spellcheck_config, "lang", Singleton::Config::source()->spellcheck_language.c_str());
  }

  spellcheck_possible_err=new_aspell_speller(spellcheck_config);
  spellcheck_checker=NULL;
  if (aspell_error_number(spellcheck_possible_err) != 0)
    std::cerr << "Spell check error: " << aspell_error_message(spellcheck_possible_err) << std::endl;
  else {
    spellcheck_checker = to_aspell_speller(spellcheck_possible_err);

    auto tag=get_buffer()->create_tag("spellcheck_error");
    tag->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    
    get_buffer()->signal_changed().connect([this](){
      delayed_spellcheck_suggestions_connection.disconnect();
      
      auto iter=get_buffer()->get_insert()->get_iter();
      bool ends_line=iter.ends_line();
      if(iter.backward_char()) {
        auto context_iter=iter;
        bool backward_success=true;
        if(ends_line || *iter=='/' || *iter=='*') //iter_has_context_class is sadly bugged
          backward_success=context_iter.backward_char();
        if(backward_success) {
          if((spellcheck_all && !get_source_buffer()->iter_has_context_class(context_iter, "no-spell-check")) || get_source_buffer()->iter_has_context_class(context_iter, "comment") || get_source_buffer()->iter_has_context_class(context_iter, "string")) {
            if(last_keyval_is_backspace && !is_word_iter(iter) && iter.forward_char()) {} //backspace fix
            if(!is_word_iter(iter)) { //Might have used space or - to split two words
              auto first=iter;
              auto second=iter;
              if(first.backward_char() && second.forward_char() && !second.starts_line()) {
                get_buffer()->remove_tag_by_name("spellcheck_error", first, second);
                auto word=spellcheck_get_word(first);
                spellcheck_word(word.first, word.second);
                word=spellcheck_get_word(second);
                spellcheck_word(word.first, word.second);
              }
            }
            else {
              auto word=spellcheck_get_word(iter);
              spellcheck_word(word.first, word.second);
            }
          }
        }
      }
    });
    
    get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator& iter, const Glib::RefPtr<Gtk::TextBuffer::Mark>& mark) {
      if(mark->get_name()=="insert") {
        if(spellcheck_suggestions_dialog_shown)
          spellcheck_suggestions_dialog->hide();
        delayed_spellcheck_suggestions_connection.disconnect();
        delayed_spellcheck_suggestions_connection=Glib::signal_timeout().connect([this]() {
          auto tags=get_buffer()->get_insert()->get_iter().get_tags();
          bool need_suggestions=false;
          for(auto &tag: tags) {
            if(tag->property_name()=="spellcheck_error") {
              need_suggestions=true;
              break;
            }
          }
          if(need_suggestions) {
            spellcheck_suggestions_dialog=std::unique_ptr<SelectionDialog>(new SelectionDialog(*this, get_buffer()->create_mark(get_buffer()->get_insert()->get_iter()), false));
            spellcheck_suggestions_dialog->on_hide=[this](){
              spellcheck_suggestions_dialog_shown=false;
            };
            auto word=spellcheck_get_word(get_buffer()->get_insert()->get_iter());
            auto suggestions=spellcheck_get_suggestions(word.first, word.second);
            if(suggestions.size()==0)
              return false;
            for(auto &suggestion: suggestions)
              spellcheck_suggestions_dialog->add_row(suggestion);
            spellcheck_suggestions_dialog->on_select=[this, word](const std::string& selected, bool hide_window) {
              get_source_buffer()->begin_user_action();
              get_buffer()->erase(word.first, word.second);
              get_buffer()->insert(get_buffer()->get_insert()->get_iter(), selected);
              get_source_buffer()->end_user_action();
              delayed_tooltips_connection.disconnect();
            };
            spellcheck_suggestions_dialog->show();
            spellcheck_suggestions_dialog_shown=true;
          }
          return false;
        }, 500);
      }
    });
  }
  
  get_buffer()->signal_changed().connect([this](){
    set_info(info);
  });
  
  set_tooltip_events();
}

void Source::View::set_tooltip_events() {
  signal_motion_notify_event().connect([this](GdkEventMotion* event) {
    if(on_motion_last_x!=event->x || on_motion_last_y!=event->y) {
      delayed_tooltips_connection.disconnect();
      if((event->state&GDK_BUTTON1_MASK)==0) {
        gdouble x=event->x;
        gdouble y=event->y;
        delayed_tooltips_connection=Glib::signal_timeout().connect([this, x, y]() {
          Tooltips::init();
          Gdk::Rectangle rectangle(x, y, 1, 1);
          if(source_readable) {
            type_tooltips.show(rectangle);
            diagnostic_tooltips.show(rectangle);
          }
          return false;
        }, 100);
      }
      type_tooltips.hide();
      diagnostic_tooltips.hide();
    }
    on_motion_last_x=event->x;
    on_motion_last_y=event->y;
    return false;
  });
  
  get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator& iterator, const Glib::RefPtr<Gtk::TextBuffer::Mark>& mark) {
    if(get_buffer()->get_has_selection() && mark->get_name()=="selection_bound")
      delayed_tooltips_connection.disconnect();
    
    if(mark->get_name()=="insert") {
      delayed_tooltips_connection.disconnect();
      delayed_tooltips_connection=Glib::signal_timeout().connect([this]() {
        Tooltips::init();
        Gdk::Rectangle rectangle;
        get_iter_location(get_buffer()->get_insert()->get_iter(), rectangle);
        int location_window_x, location_window_y;
        buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, rectangle.get_x(), rectangle.get_y(), location_window_x, location_window_y);
        rectangle.set_x(location_window_x-2);
        rectangle.set_y(location_window_y);
        rectangle.set_width(4);
        if(source_readable) {
          type_tooltips.show(rectangle);
          diagnostic_tooltips.show(rectangle);
        }
        return false;
      }, 500);
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      set_info(info);
    }
  });
  
  signal_scroll_event().connect([this](GdkEventScroll* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    return false;
  });
  
  signal_focus_out_event().connect([this](GdkEventFocus* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    return false;
  });
}

void Source::View::search_occurrences_updated(GtkWidget* widget, GParamSpec* property, gpointer data) {
  auto view=(Source::View*)data;
  if(view->update_search_occurrences)
    view->update_search_occurrences(gtk_source_search_context_get_occurrences_count(view->search_context));
}

Source::View::~View() {
  g_clear_object(&search_context);
  g_clear_object(&search_settings);
  
  delayed_tooltips_connection.disconnect();
  delayed_spellcheck_suggestions_connection.disconnect();
  
  if(spellcheck_checker!=NULL)
    delete_aspell_speller(spellcheck_checker);
}

void Source::View::search_highlight(const std::string &text, bool case_sensitive, bool regex) {
  gtk_source_search_settings_set_case_sensitive(search_settings, case_sensitive);
  gtk_source_search_settings_set_regex_enabled(search_settings, regex);
  gtk_source_search_settings_set_search_text(search_settings, text.c_str());
  search_occurrences_updated(NULL, NULL, this);
}

void Source::View::search_forward() {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto& start=selection_bound;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_forward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    get_buffer()->select_range(match_start, match_end);
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::search_backward() {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=insert;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_backward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    get_buffer()->select_range(match_start, match_end);
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_forward(const std::string &replacement) {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=insert;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_forward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    auto offset=match_start.get_offset();
    gtk_source_search_context_replace(search_context, match_start.gobj(), match_end.gobj(), replacement.c_str(), replacement.size(), NULL);
    
    get_buffer()->select_range(get_buffer()->get_iter_at_offset(offset), get_buffer()->get_iter_at_offset(offset+replacement.size()));
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_backward(const std::string &replacement) {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=selection_bound;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_backward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
  auto offset=match_start.get_offset();
    gtk_source_search_context_replace(search_context, match_start.gobj(), match_end.gobj(), replacement.c_str(), replacement.size(), NULL);

    get_buffer()->select_range(get_buffer()->get_iter_at_offset(offset), get_buffer()->get_iter_at_offset(offset+replacement.size()));
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_all(const std::string &replacement) {
  gtk_source_search_context_replace_all(search_context, replacement.c_str(), replacement.size(), NULL);
}

void Source::View::paste() {
  auto text=Gtk::Clipboard::get()->wait_for_text();
  //replace stand-alone carriage returns (which makes clang return wrong line index) with newlines
  for(auto it=text.begin();it!=text.end();it++) {
    if(*it=='\r') {
      auto it2=it;
      it2++;
      if(it2==text.end() || *it2!='\n')
        text.replace(it, it2, "\n");
    }
  }

  auto line=get_line_before();
  std::smatch sm;
  std::string prefix_tabs;
  if(!get_buffer()->get_has_selection() && std::regex_match(line, sm, tabs_regex) && sm[2].str().size()==0) {
    prefix_tabs=sm[1].str();

    Glib::ustring::size_type start_line=0;
    Glib::ustring::size_type end_line=0;
    bool paste_line=false;
    bool first_paste_line=true;
    size_t paste_line_tabs=-1;
    bool first_paste_line_has_tabs=false;
    for(Glib::ustring::size_type c=0;c<text.size();c++) {;
      if(text[c]=='\n') {
        end_line=c;
        paste_line=true;
      }
      else if(c==text.size()-1) {
        end_line=c+1;
        paste_line=true;
      }
      if(paste_line) {
        bool empty_line=true;
        std::string line=text.substr(start_line, end_line-start_line);
        size_t tabs=0;
        for(auto chr: line) {
          if(chr==tab_char)
            tabs++;
          else {
            empty_line=false;
            break;
          }
        }
        if(first_paste_line) {
          if(tabs!=0) {
            first_paste_line_has_tabs=true;
            paste_line_tabs=tabs;
          }
          first_paste_line=false;
        }
        else if(!empty_line)
          paste_line_tabs=std::min(paste_line_tabs, tabs);

        start_line=end_line+1;
        paste_line=false;
      }
    }
    if(paste_line_tabs==(size_t)-1)
      paste_line_tabs=0;
    start_line=0;
    end_line=0;
    paste_line=false;
    first_paste_line=true;
    get_source_buffer()->begin_user_action();
    for(Glib::ustring::size_type c=0;c<text.size();c++) {
      if(text[c]=='\n') {
        end_line=c;
        paste_line=true;
      }
      else if(c==text.size()-1) {
        end_line=c+1;
        paste_line=true;
      }
      if(paste_line) {
        std::string line=text.substr(start_line, end_line-start_line);
        size_t line_tabs=0;
        for(auto chr: line) {
          if(chr==tab_char)
            line_tabs++;
          else
            break;
        }
        auto tabs=paste_line_tabs;
        if(!(first_paste_line && !first_paste_line_has_tabs) && line_tabs<paste_line_tabs) {
          tabs=line_tabs;
        }
        
        if(first_paste_line) {
          if(first_paste_line_has_tabs)
            get_buffer()->insert_at_cursor(text.substr(start_line+tabs, end_line-start_line-tabs));
          else
            get_buffer()->insert_at_cursor(text.substr(start_line, end_line-start_line));
          first_paste_line=false;
        }
        else
          get_buffer()->insert_at_cursor('\n'+prefix_tabs+text.substr(start_line+tabs, end_line-start_line-tabs));
        start_line=end_line+1;
        paste_line=false;
      }
    }
    get_buffer()->place_cursor(get_buffer()->get_insert()->get_iter());
    scroll_to(get_buffer()->get_insert());
    get_source_buffer()->end_user_action();
  }
  else {
    Gtk::Clipboard::get()->set_text(text);
    get_buffer()->paste_clipboard(Gtk::Clipboard::get());
  }
}

void Source::View::set_status(const std::string &status) {
  this->status=status;
  if(on_update_status)
    on_update_status(this, status);
}

void Source::View::set_info(const std::string &info) {
  this->info=info;
  auto iter=get_buffer()->get_insert()->get_iter();
  auto positions=std::to_string(iter.get_line()+1)+":"+std::to_string(iter.get_line_offset()+1);
  if(on_update_info)
    on_update_info(this, positions+" "+info);
}

std::string Source::View::get_line(const Gtk::TextIter &iter) {
  auto line_start_it = get_buffer()->get_iter_at_line(iter.get_line());
  auto line_end_it = iter;
  while(!line_end_it.ends_line() && line_end_it.forward_char()) {}
  std::string line(get_source_buffer()->get_text(line_start_it, line_end_it));
  return line;
}

std::string Source::View::get_line(Glib::RefPtr<Gtk::TextBuffer::Mark> mark) {
  return get_line(mark->get_iter());
}

std::string Source::View::get_line(int line_nr) {
  return get_line(get_buffer()->get_iter_at_line(line_nr));
}

std::string Source::View::get_line() {
  return get_line(get_buffer()->get_insert());
}

std::string Source::View::get_line_before(const Gtk::TextIter &iter) {
  auto line_it = get_source_buffer()->get_iter_at_line(iter.get_line());
  std::string line(get_source_buffer()->get_text(line_it, iter));
  return line;
}

std::string Source::View::get_line_before(Glib::RefPtr<Gtk::TextBuffer::Mark> mark) {
  return get_line_before(mark->get_iter());
}

std::string Source::View::get_line_before() {
  return get_line_before(get_buffer()->get_insert());
}

bool Source::View::find_start_of_closed_expression(Gtk::TextIter iter, Gtk::TextIter &found_iter) {
  int count1=0;
  int count2=0;
  
  bool ignore=false;
  
  do {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter==')')
          count1++;
        else if(*iter==']')
          count2++;
        else if(*iter=='(')
          count1--;
        else if(*iter=='[')
          count2--;
      }
    }
    if(iter.starts_line() && count1<=0 && count2<=0) {
      auto insert_iter=get_buffer()->get_insert()->get_iter();
      while(iter!=insert_iter && *iter==static_cast<unsigned char>(tab_char) && iter.forward_char()) {}
      found_iter=iter;
      return true;
    }
  } while(iter.backward_char());
  return false;
}

bool Source::View::find_open_expression_symbol(Gtk::TextIter iter, const Gtk::TextIter &until_iter, Gtk::TextIter &found_iter) {
  int count1=0;
  int count2=0;

  bool ignore=false;
  
  while(iter!=until_iter && iter.backward_char()) {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter==')')
          count1++;
        else if(*iter==']')
          count2++;
        else if(*iter=='(')
          count1--;
        else if(*iter=='[')
          count2--;
      }
      if(count1<0 || count2<0) {
        found_iter=iter;
        return true;
      }
    }
  }
  
  return false;
}  

bool Source::View::find_right_bracket_forward(Gtk::TextIter iter, Gtk::TextIter &found_iter) {
  int count=0;
  
  bool ignore=false;
  
  while(iter.forward_char()) {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter=='}') {
          if(count==0) {
            found_iter=iter;
            return true;
          }
          count--;
        }
        else if(*iter=='{')
          count++;
      }
    }
  }
  return false;
}

//Basic indentation
bool Source::View::on_key_press_event(GdkEventKey* key) {
  if(spellcheck_suggestions_dialog_shown) {
    if(spellcheck_suggestions_dialog->on_key_press(key))
      return true;
  }

  if(key->keyval==GDK_KEY_BackSpace)
    last_keyval_is_backspace=true;
  else
    last_keyval_is_backspace=false;
  
  get_source_buffer()->begin_user_action();
  //Indent as in next or previous line
  if(key->keyval==GDK_KEY_Return && !get_buffer()->get_has_selection()) {
    auto insert_it=get_buffer()->get_insert()->get_iter();
    int line_nr=insert_it.get_line();
    auto line=get_line_before();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex)) {
      if((line_nr+1)<get_buffer()->get_line_count()) {
        string next_line=get_line(line_nr+1);
        auto line_end_iter=get_buffer()->get_iter_at_line(line_nr+1);
        if(line_end_iter.backward_char()) {
          std::smatch sm2;
          if(insert_it==line_end_iter && std::regex_match(next_line, sm2, tabs_regex)) {
            if(sm2[1].str().size()>sm[1].str().size()) {
              get_source_buffer()->insert_at_cursor("\n"+sm2[1].str());
              scroll_to(get_source_buffer()->get_insert());
              get_source_buffer()->end_user_action();
              return true;
            }
          }
        }
      }
      get_source_buffer()->insert_at_cursor("\n"+sm[1].str());
      scroll_to(get_source_buffer()->get_insert());
      get_source_buffer()->end_user_action();
      return true;
    }
  }
  //Indent right when clicking tab, no matter where in the line the cursor is. Also works on selected text.
  else if(key->keyval==GDK_KEY_Tab) {
    Gtk::TextIter selection_start, selection_end;
    get_source_buffer()->get_selection_bounds(selection_start, selection_end);
    int line_start=selection_start.get_line();
    int line_end=selection_end.get_line();
    for(int line=line_start;line<=line_end;line++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line);
      if(!get_buffer()->get_has_selection() || line_it!=selection_end)
        get_source_buffer()->insert(line_it, tab);
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //Indent left when clicking shift-tab, no matter where in the line the cursor is. Also works on selected text.
  else if((key->keyval==GDK_KEY_ISO_Left_Tab || key->keyval==GDK_KEY_Tab) && (key->state&GDK_SHIFT_MASK)>0) {
    Gtk::TextIter selection_start, selection_end;
    get_source_buffer()->get_selection_bounds(selection_start, selection_end);
    int line_start=selection_start.get_line();
    int line_end=selection_end.get_line();
    
    unsigned indent_left_steps=tab_size;
    std::vector<bool> ignore_line;
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      auto line_it = get_source_buffer()->get_iter_at_line(line_nr);
      if(!get_buffer()->get_has_selection() || line_it!=selection_end) {
        string line=get_line(line_nr);
        std::smatch sm;
        if(std::regex_match(line, sm, tabs_regex) && (sm[1].str().size()>0 || sm[2].str().size()==0)) {
          if(sm[2].str().size()>0) {
            indent_left_steps=std::min(indent_left_steps, (unsigned)sm[1].str().size());
            ignore_line.push_back(false);
          }
          else if((unsigned)sm[1].str().size()<indent_left_steps)
            ignore_line.push_back(true);
          else
            ignore_line.push_back(false);
        }
        else {
          get_source_buffer()->end_user_action();
          return true;
        }
      }
    }
    
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line_nr);
      Gtk::TextIter line_plus_it=line_it;
      if(!get_buffer()->get_has_selection() || line_it!=selection_end) {
        if(indent_left_steps==0 || line_plus_it.forward_chars(indent_left_steps))
          if(!ignore_line.at(line_nr-line_start))
            get_source_buffer()->erase(line_it, line_plus_it);
      }
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //"Smart" backspace key TODO: remove commented out lines if this works
  else if(key->keyval==GDK_KEY_BackSpace && !get_buffer()->get_has_selection()) {
    auto insert_it=get_buffer()->get_insert()->get_iter();
    //int line_nr=insert_it.get_line();
    auto line=get_line_before();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()==line.size()) {
      auto line_start_iter=insert_it;
      if(line_start_iter.backward_chars(line.size()))
        get_buffer()->erase(insert_it, line_start_iter);
    }
  }

  bool stop=Gsv::View::on_key_press_event(key);
  get_source_buffer()->end_user_action();
  return stop;
}

bool Source::View::on_button_press_event(GdkEventButton *event) {
  if(event->type==GDK_2BUTTON_PRESS) {
    Gtk::TextIter start, end;
    get_buffer()->get_selection_bounds(start, end);
    auto iter=start;
    while((*iter>=48 && *iter<=57) || (*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter==95) {
      start=iter;
      if(!iter.backward_char())
        break;
    }
    while((*end>=48 && *end<=57) || (*end>=65 && *end<=90) || (*end>=97 && *end<=122) || *end==95) {
      if(!end.forward_char())
        break;
    }
    get_buffer()->select_range(start, end);
    return true;
  }
  
  return Gsv::View::on_button_press_event(event);
}

std::pair<char, unsigned> Source::View::find_tab_char_and_size() {
  const std::regex indent_regex("^([ \t]+)(.*)$");
  auto size=get_buffer()->get_line_count();
  std::unordered_map<char, size_t> tab_chars;
  std::unordered_map<unsigned, size_t> tab_sizes;
  unsigned last_tab_size=0;
  for(int c=0;c<size;c++) {
    auto line=get_line(c);
    std::smatch sm;
    std::string str;
    if(std::regex_match(line, sm, indent_regex)) {
      if(sm[2].str().size()==0)
        continue;
      str=sm[1].str();
    }
    else {
      str="";
      if(line.size()==0)
        continue;
    }
    
    long tab_diff=abs(static_cast<long>(str.size()-last_tab_size));
    if(tab_diff>0) {
      unsigned tab_diff_unsigned=static_cast<unsigned>(tab_diff);
      auto it_size=tab_sizes.find(tab_diff_unsigned);
      if(it_size!=tab_sizes.end())
        it_size->second++;
      else
        tab_sizes[tab_diff_unsigned]=1;
    }
    
    last_tab_size=str.size();
    
    if(str.size()>0) {
      auto it_char=tab_chars.find(str[0]);
      if(it_char!=tab_chars.end())
        it_char->second++;
      else
        tab_chars[str[0]]=1;
    }
  }
  char found_tab_char=0;
  size_t occurences=0;
  for(auto &tab_char: tab_chars) {
    if(tab_char.second>occurences) {
      found_tab_char=tab_char.first;
      occurences=tab_char.second;
    }
  }
  unsigned found_tab_size=0;
  occurences=0;
  for(auto &tab_size: tab_sizes) {
    if(tab_size.second>occurences) {
      found_tab_size=tab_size.first;
      occurences=tab_size.second;
    }
  }
  return {found_tab_char, found_tab_size};
}

bool Source::View::is_word_iter(const Gtk::TextIter& iter) {
  return ((*iter>=48 && *iter<=57) || (*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter==39 || *iter>=128);
}

std::pair<Gtk::TextIter, Gtk::TextIter> Source::View::spellcheck_get_word(Gtk::TextIter iter) {
  auto start=iter;
  auto end=iter;
  
  while(is_word_iter(iter)) {
    start=iter;<commit_msg>Numbers are no longer included in spellcheck.<commit_after>#include "source.h"
#include "sourcefile.h"
#include <boost/property_tree/json_parser.hpp>
#include <boost/timer/timer.hpp>
#include "logging.h"
#include <algorithm>
#include "singletons.h"
#include <gtksourceview/gtksource.h>
#include <boost/lexical_cast.hpp>
#include <iostream>

using namespace std; //TODO: remove

namespace sigc {
#ifndef SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
  template <typename Functor>
  struct functor_trait<Functor, false> {
    typedef decltype (::sigc::mem_fun(std::declval<Functor&>(),
                                      &Functor::operator())) _intermediate;
    typedef typename _intermediate::result_type result_type;
    typedef Functor functor_type;
  };
#else
  SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
#endif
}

Glib::RefPtr<Gsv::Language> Source::guess_language(const boost::filesystem::path &file_path) {
  auto language_manager=Gsv::LanguageManager::get_default();
  bool result_uncertain = false;
  auto content_type = Gio::content_type_guess(file_path.string(), NULL, 0, result_uncertain);
  if(result_uncertain) {
    content_type.clear();
  }
  auto language=language_manager->guess_language(file_path.string(), content_type);
  if(!language) {
    auto filename=file_path.filename().string();
    if(filename=="CMakeLists.txt")
      language=language_manager->get_language("cmake");
    else if(filename=="Makefile")
      language=language_manager->get_language("makefile");
  }
  return language;
}

//////////////
//// View ////
//////////////
AspellConfig* Source::View::spellcheck_config=NULL;

Source::View::View(const boost::filesystem::path &file_path, Glib::RefPtr<Gsv::Language> language): file_path(file_path) {
  set_smart_home_end(Gsv::SMART_HOME_END_BEFORE);
  
  get_source_buffer()->begin_not_undoable_action();
  if(language) {
    if(juci::filesystem::read_non_utf8(file_path, get_buffer())==-1)
      Singleton::terminal()->print("Warning: "+file_path.string()+" is not a valid UTF-8 file. Saving might corrupt the file.\n");
  }
  else {
    if(juci::filesystem::read(file_path, get_buffer())==-1)
      Singleton::terminal()->print("Error: "+file_path.string()+" is not a valid UTF-8 file.\n");
  }
  get_source_buffer()->end_not_undoable_action();
  
  get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(0)); 
  
  search_settings = gtk_source_search_settings_new();
  gtk_source_search_settings_set_wrap_around(search_settings, true);
  search_context = gtk_source_search_context_new(get_source_buffer()->gobj(), search_settings);
  gtk_source_search_context_set_highlight(search_context, true);
  //TODO: why does this not work?: Might be best to use the styles from sourceview. These has to be read from file, search-matches got style "search-match"
  //TODO: in header if trying again: GtkSourceStyle* search_match_style;
  //TODO: We can drop this, only work on newer versions of gtksourceview.
  //search_match_style=(GtkSourceStyle*)g_object_new(GTK_SOURCE_TYPE_STYLE, "background-set", 1, "background", "#00FF00", NULL);
  //gtk_source_search_context_set_match_style(search_context, search_match_style);
  
  //TODO: either use lambda if possible or create a gtkmm wrapper around search_context (including search_settings):
  //TODO: (gtkmm's Gtk::Object has connect_property_changed, so subclassing this might be an idea)
  g_signal_connect(search_context, "notify::occurrences-count", G_CALLBACK(search_occurrences_updated), this);
  
  //TODO: Move this to notebook? Might take up too much memory doing this for every tab.
  auto style_scheme_manager=Gsv::StyleSchemeManager::get_default();
  style_scheme_manager->prepend_search_path(Singleton::style_dir());

  if(Singleton::Config::source()->style.size()>0) {
    auto scheme = style_scheme_manager->get_scheme(Singleton::Config::source()->style);

    if(scheme)
      get_source_buffer()->set_style_scheme(scheme);
    else
      Singleton::terminal()->print("Error: Could not find gtksourceview style: "+Singleton::Config::source()->style+'\n');
  }
  
  if(Singleton::Config::source()->wrap_lines)
    set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
  property_highlight_current_line() = Singleton::Config::source()->highlight_current_line;
  property_show_line_numbers() = Singleton::Config::source()->show_line_numbers;
  if(Singleton::Config::source()->font.size()>0)
    override_font(Pango::FontDescription(Singleton::Config::source()->font));
  
  //Create tags for diagnostic warnings and errors:
  auto scheme = get_source_buffer()->get_style_scheme();
  auto tag_table=get_buffer()->get_tag_table();
  auto style=scheme->get_style("def:warning");
  auto diagnostic_tag=get_source_buffer()->create_tag("def:warning");
  auto diagnostic_tag_underline=get_source_buffer()->create_tag("def:warning_underline");
  if(style && (style->property_foreground_set() || style->property_background_set())) {
    Glib::ustring warning_property;
    if(style->property_foreground_set()) {
      warning_property=style->property_foreground().get_value();
      diagnostic_tag->property_foreground() = warning_property;
    }
    else if(style->property_background_set())
      warning_property=style->property_background().get_value();

    diagnostic_tag_underline->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    auto tag_class=G_OBJECT_GET_CLASS(diagnostic_tag_underline->gobj()); //For older GTK+ 3 versions:
    auto param_spec=g_object_class_find_property(tag_class, "underline-rgba");
    if(param_spec!=NULL) {
      diagnostic_tag_underline->set_property("underline-rgba", Gdk::RGBA(warning_property));
    }
  }
  style=scheme->get_style("def:error");
  diagnostic_tag=get_source_buffer()->create_tag("def:error");
  diagnostic_tag_underline=get_source_buffer()->create_tag("def:error_underline");
  if(style && (style->property_foreground_set() || style->property_background_set())) {
    Glib::ustring error_property;
    if(style->property_foreground_set()) {
      error_property=style->property_foreground().get_value();
      diagnostic_tag->property_foreground() = error_property;
    }
    else if(style->property_background_set())
      error_property=style->property_background().get_value();
    
    diagnostic_tag_underline->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    auto tag_class=G_OBJECT_GET_CLASS(diagnostic_tag_underline->gobj()); //For older GTK+ 3 versions:
    auto param_spec=g_object_class_find_property(tag_class, "underline-rgba");
    if(param_spec!=NULL) {
      diagnostic_tag_underline->set_property("underline-rgba", Gdk::RGBA(error_property));
    }
  }
  //TODO: clear tag_class and param_spec?
  
  //Add tooltip foreground and background
  style = scheme->get_style("def:note");
  auto note_tag=get_source_buffer()->create_tag("def:note_background");
  if(style->property_background_set()) {
    note_tag->property_background()=style->property_background();
  }
  note_tag=get_source_buffer()->create_tag("def:note");
  if(style->property_foreground_set()) {
    note_tag->property_foreground()=style->property_foreground();
  }
  
  tab_char=Singleton::Config::source()->default_tab_char;
  tab_size=Singleton::Config::source()->default_tab_size;
  if(Singleton::Config::source()->auto_tab_char_and_size) {
    auto tab_char_and_size=find_tab_char_and_size();
    if(tab_char_and_size.first!=0) {
      if(tab_char!=tab_char_and_size.first || tab_size!=tab_char_and_size.second) {
        std::string tab_str;
        if(tab_char_and_size.first==' ')
          tab_str="<space>";
        else
          tab_str="<tab>";
        Singleton::terminal()->print("Tab char and size for file "+file_path.string()+" set to: "+tab_str+", "+boost::lexical_cast<std::string>(tab_char_and_size.second)+".\n");
      }
      
      tab_char=tab_char_and_size.first;
      tab_size=tab_char_and_size.second;
    }
  }
  for(unsigned c=0;c<tab_size;c++)
    tab+=tab_char;
  
  tabs_regex=std::regex(std::string("^(")+tab_char+"*)(.*)$");
  
  if(spellcheck_config==NULL) {
    spellcheck_config=new_aspell_config();
    if(Singleton::Config::source()->spellcheck_language.size()>0)
      aspell_config_replace(spellcheck_config, "lang", Singleton::Config::source()->spellcheck_language.c_str());
  }

  spellcheck_possible_err=new_aspell_speller(spellcheck_config);
  spellcheck_checker=NULL;
  if (aspell_error_number(spellcheck_possible_err) != 0)
    std::cerr << "Spell check error: " << aspell_error_message(spellcheck_possible_err) << std::endl;
  else {
    spellcheck_checker = to_aspell_speller(spellcheck_possible_err);

    auto tag=get_buffer()->create_tag("spellcheck_error");
    tag->property_underline()=Pango::Underline::UNDERLINE_ERROR;
    
    get_buffer()->signal_changed().connect([this](){
      delayed_spellcheck_suggestions_connection.disconnect();
      
      auto iter=get_buffer()->get_insert()->get_iter();
      bool ends_line=iter.ends_line();
      if(iter.backward_char()) {
        auto context_iter=iter;
        bool backward_success=true;
        if(ends_line || *iter=='/' || *iter=='*') //iter_has_context_class is sadly bugged
          backward_success=context_iter.backward_char();
        if(backward_success) {
          if((spellcheck_all && !get_source_buffer()->iter_has_context_class(context_iter, "no-spell-check")) || get_source_buffer()->iter_has_context_class(context_iter, "comment") || get_source_buffer()->iter_has_context_class(context_iter, "string")) {
            if(last_keyval_is_backspace && !is_word_iter(iter) && iter.forward_char()) {} //backspace fix
            if(!is_word_iter(iter)) { //Might have used space or - to split two words
              auto first=iter;
              auto second=iter;
              if(first.backward_char() && second.forward_char() && !second.starts_line()) {
                get_buffer()->remove_tag_by_name("spellcheck_error", first, second);
                auto word=spellcheck_get_word(first);
                spellcheck_word(word.first, word.second);
                word=spellcheck_get_word(second);
                spellcheck_word(word.first, word.second);
              }
            }
            else {
              auto word=spellcheck_get_word(iter);
              spellcheck_word(word.first, word.second);
            }
          }
        }
      }
    });
    
    get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator& iter, const Glib::RefPtr<Gtk::TextBuffer::Mark>& mark) {
      if(mark->get_name()=="insert") {
        if(spellcheck_suggestions_dialog_shown)
          spellcheck_suggestions_dialog->hide();
        delayed_spellcheck_suggestions_connection.disconnect();
        delayed_spellcheck_suggestions_connection=Glib::signal_timeout().connect([this]() {
          auto tags=get_buffer()->get_insert()->get_iter().get_tags();
          bool need_suggestions=false;
          for(auto &tag: tags) {
            if(tag->property_name()=="spellcheck_error") {
              need_suggestions=true;
              break;
            }
          }
          if(need_suggestions) {
            spellcheck_suggestions_dialog=std::unique_ptr<SelectionDialog>(new SelectionDialog(*this, get_buffer()->create_mark(get_buffer()->get_insert()->get_iter()), false));
            spellcheck_suggestions_dialog->on_hide=[this](){
              spellcheck_suggestions_dialog_shown=false;
            };
            auto word=spellcheck_get_word(get_buffer()->get_insert()->get_iter());
            auto suggestions=spellcheck_get_suggestions(word.first, word.second);
            if(suggestions.size()==0)
              return false;
            for(auto &suggestion: suggestions)
              spellcheck_suggestions_dialog->add_row(suggestion);
            spellcheck_suggestions_dialog->on_select=[this, word](const std::string& selected, bool hide_window) {
              get_source_buffer()->begin_user_action();
              get_buffer()->erase(word.first, word.second);
              get_buffer()->insert(get_buffer()->get_insert()->get_iter(), selected);
              get_source_buffer()->end_user_action();
              delayed_tooltips_connection.disconnect();
            };
            spellcheck_suggestions_dialog->show();
            spellcheck_suggestions_dialog_shown=true;
          }
          return false;
        }, 500);
      }
    });
  }
  
  get_buffer()->signal_changed().connect([this](){
    set_info(info);
  });
  
  set_tooltip_events();
}

void Source::View::set_tooltip_events() {
  signal_motion_notify_event().connect([this](GdkEventMotion* event) {
    if(on_motion_last_x!=event->x || on_motion_last_y!=event->y) {
      delayed_tooltips_connection.disconnect();
      if((event->state&GDK_BUTTON1_MASK)==0) {
        gdouble x=event->x;
        gdouble y=event->y;
        delayed_tooltips_connection=Glib::signal_timeout().connect([this, x, y]() {
          Tooltips::init();
          Gdk::Rectangle rectangle(x, y, 1, 1);
          if(source_readable) {
            type_tooltips.show(rectangle);
            diagnostic_tooltips.show(rectangle);
          }
          return false;
        }, 100);
      }
      type_tooltips.hide();
      diagnostic_tooltips.hide();
    }
    on_motion_last_x=event->x;
    on_motion_last_y=event->y;
    return false;
  });
  
  get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator& iterator, const Glib::RefPtr<Gtk::TextBuffer::Mark>& mark) {
    if(get_buffer()->get_has_selection() && mark->get_name()=="selection_bound")
      delayed_tooltips_connection.disconnect();
    
    if(mark->get_name()=="insert") {
      delayed_tooltips_connection.disconnect();
      delayed_tooltips_connection=Glib::signal_timeout().connect([this]() {
        Tooltips::init();
        Gdk::Rectangle rectangle;
        get_iter_location(get_buffer()->get_insert()->get_iter(), rectangle);
        int location_window_x, location_window_y;
        buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, rectangle.get_x(), rectangle.get_y(), location_window_x, location_window_y);
        rectangle.set_x(location_window_x-2);
        rectangle.set_y(location_window_y);
        rectangle.set_width(4);
        if(source_readable) {
          type_tooltips.show(rectangle);
          diagnostic_tooltips.show(rectangle);
        }
        return false;
      }, 500);
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      set_info(info);
    }
  });
  
  signal_scroll_event().connect([this](GdkEventScroll* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    return false;
  });
  
  signal_focus_out_event().connect([this](GdkEventFocus* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    return false;
  });
}

void Source::View::search_occurrences_updated(GtkWidget* widget, GParamSpec* property, gpointer data) {
  auto view=(Source::View*)data;
  if(view->update_search_occurrences)
    view->update_search_occurrences(gtk_source_search_context_get_occurrences_count(view->search_context));
}

Source::View::~View() {
  g_clear_object(&search_context);
  g_clear_object(&search_settings);
  
  delayed_tooltips_connection.disconnect();
  delayed_spellcheck_suggestions_connection.disconnect();
  
  if(spellcheck_checker!=NULL)
    delete_aspell_speller(spellcheck_checker);
}

void Source::View::search_highlight(const std::string &text, bool case_sensitive, bool regex) {
  gtk_source_search_settings_set_case_sensitive(search_settings, case_sensitive);
  gtk_source_search_settings_set_regex_enabled(search_settings, regex);
  gtk_source_search_settings_set_search_text(search_settings, text.c_str());
  search_occurrences_updated(NULL, NULL, this);
}

void Source::View::search_forward() {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto& start=selection_bound;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_forward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    get_buffer()->select_range(match_start, match_end);
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::search_backward() {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=insert;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_backward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    get_buffer()->select_range(match_start, match_end);
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_forward(const std::string &replacement) {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=insert;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_forward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
    auto offset=match_start.get_offset();
    gtk_source_search_context_replace(search_context, match_start.gobj(), match_end.gobj(), replacement.c_str(), replacement.size(), NULL);
    
    get_buffer()->select_range(get_buffer()->get_iter_at_offset(offset), get_buffer()->get_iter_at_offset(offset+replacement.size()));
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_backward(const std::string &replacement) {
  Gtk::TextIter insert, selection_bound;
  get_buffer()->get_selection_bounds(insert, selection_bound);
  auto &start=selection_bound;
  Gtk::TextIter match_start, match_end;
  if(gtk_source_search_context_backward(search_context, start.gobj(), match_start.gobj(), match_end.gobj())) {
  auto offset=match_start.get_offset();
    gtk_source_search_context_replace(search_context, match_start.gobj(), match_end.gobj(), replacement.c_str(), replacement.size(), NULL);

    get_buffer()->select_range(get_buffer()->get_iter_at_offset(offset), get_buffer()->get_iter_at_offset(offset+replacement.size()));
    scroll_to(get_buffer()->get_insert());
  }
}

void Source::View::replace_all(const std::string &replacement) {
  gtk_source_search_context_replace_all(search_context, replacement.c_str(), replacement.size(), NULL);
}

void Source::View::paste() {
  auto text=Gtk::Clipboard::get()->wait_for_text();
  //replace stand-alone carriage returns (which makes clang return wrong line index) with newlines
  for(auto it=text.begin();it!=text.end();it++) {
    if(*it=='\r') {
      auto it2=it;
      it2++;
      if(it2==text.end() || *it2!='\n')
        text.replace(it, it2, "\n");
    }
  }

  auto line=get_line_before();
  std::smatch sm;
  std::string prefix_tabs;
  if(!get_buffer()->get_has_selection() && std::regex_match(line, sm, tabs_regex) && sm[2].str().size()==0) {
    prefix_tabs=sm[1].str();

    Glib::ustring::size_type start_line=0;
    Glib::ustring::size_type end_line=0;
    bool paste_line=false;
    bool first_paste_line=true;
    size_t paste_line_tabs=-1;
    bool first_paste_line_has_tabs=false;
    for(Glib::ustring::size_type c=0;c<text.size();c++) {;
      if(text[c]=='\n') {
        end_line=c;
        paste_line=true;
      }
      else if(c==text.size()-1) {
        end_line=c+1;
        paste_line=true;
      }
      if(paste_line) {
        bool empty_line=true;
        std::string line=text.substr(start_line, end_line-start_line);
        size_t tabs=0;
        for(auto chr: line) {
          if(chr==tab_char)
            tabs++;
          else {
            empty_line=false;
            break;
          }
        }
        if(first_paste_line) {
          if(tabs!=0) {
            first_paste_line_has_tabs=true;
            paste_line_tabs=tabs;
          }
          first_paste_line=false;
        }
        else if(!empty_line)
          paste_line_tabs=std::min(paste_line_tabs, tabs);

        start_line=end_line+1;
        paste_line=false;
      }
    }
    if(paste_line_tabs==(size_t)-1)
      paste_line_tabs=0;
    start_line=0;
    end_line=0;
    paste_line=false;
    first_paste_line=true;
    get_source_buffer()->begin_user_action();
    for(Glib::ustring::size_type c=0;c<text.size();c++) {
      if(text[c]=='\n') {
        end_line=c;
        paste_line=true;
      }
      else if(c==text.size()-1) {
        end_line=c+1;
        paste_line=true;
      }
      if(paste_line) {
        std::string line=text.substr(start_line, end_line-start_line);
        size_t line_tabs=0;
        for(auto chr: line) {
          if(chr==tab_char)
            line_tabs++;
          else
            break;
        }
        auto tabs=paste_line_tabs;
        if(!(first_paste_line && !first_paste_line_has_tabs) && line_tabs<paste_line_tabs) {
          tabs=line_tabs;
        }
        
        if(first_paste_line) {
          if(first_paste_line_has_tabs)
            get_buffer()->insert_at_cursor(text.substr(start_line+tabs, end_line-start_line-tabs));
          else
            get_buffer()->insert_at_cursor(text.substr(start_line, end_line-start_line));
          first_paste_line=false;
        }
        else
          get_buffer()->insert_at_cursor('\n'+prefix_tabs+text.substr(start_line+tabs, end_line-start_line-tabs));
        start_line=end_line+1;
        paste_line=false;
      }
    }
    get_buffer()->place_cursor(get_buffer()->get_insert()->get_iter());
    scroll_to(get_buffer()->get_insert());
    get_source_buffer()->end_user_action();
  }
  else {
    Gtk::Clipboard::get()->set_text(text);
    get_buffer()->paste_clipboard(Gtk::Clipboard::get());
  }
}

void Source::View::set_status(const std::string &status) {
  this->status=status;
  if(on_update_status)
    on_update_status(this, status);
}

void Source::View::set_info(const std::string &info) {
  this->info=info;
  auto iter=get_buffer()->get_insert()->get_iter();
  auto positions=std::to_string(iter.get_line()+1)+":"+std::to_string(iter.get_line_offset()+1);
  if(on_update_info)
    on_update_info(this, positions+" "+info);
}

std::string Source::View::get_line(const Gtk::TextIter &iter) {
  auto line_start_it = get_buffer()->get_iter_at_line(iter.get_line());
  auto line_end_it = iter;
  while(!line_end_it.ends_line() && line_end_it.forward_char()) {}
  std::string line(get_source_buffer()->get_text(line_start_it, line_end_it));
  return line;
}

std::string Source::View::get_line(Glib::RefPtr<Gtk::TextBuffer::Mark> mark) {
  return get_line(mark->get_iter());
}

std::string Source::View::get_line(int line_nr) {
  return get_line(get_buffer()->get_iter_at_line(line_nr));
}

std::string Source::View::get_line() {
  return get_line(get_buffer()->get_insert());
}

std::string Source::View::get_line_before(const Gtk::TextIter &iter) {
  auto line_it = get_source_buffer()->get_iter_at_line(iter.get_line());
  std::string line(get_source_buffer()->get_text(line_it, iter));
  return line;
}

std::string Source::View::get_line_before(Glib::RefPtr<Gtk::TextBuffer::Mark> mark) {
  return get_line_before(mark->get_iter());
}

std::string Source::View::get_line_before() {
  return get_line_before(get_buffer()->get_insert());
}

bool Source::View::find_start_of_closed_expression(Gtk::TextIter iter, Gtk::TextIter &found_iter) {
  int count1=0;
  int count2=0;
  
  bool ignore=false;
  
  do {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter==')')
          count1++;
        else if(*iter==']')
          count2++;
        else if(*iter=='(')
          count1--;
        else if(*iter=='[')
          count2--;
      }
    }
    if(iter.starts_line() && count1<=0 && count2<=0) {
      auto insert_iter=get_buffer()->get_insert()->get_iter();
      while(iter!=insert_iter && *iter==static_cast<unsigned char>(tab_char) && iter.forward_char()) {}
      found_iter=iter;
      return true;
    }
  } while(iter.backward_char());
  return false;
}

bool Source::View::find_open_expression_symbol(Gtk::TextIter iter, const Gtk::TextIter &until_iter, Gtk::TextIter &found_iter) {
  int count1=0;
  int count2=0;

  bool ignore=false;
  
  while(iter!=until_iter && iter.backward_char()) {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter==')')
          count1++;
        else if(*iter==']')
          count2++;
        else if(*iter=='(')
          count1--;
        else if(*iter=='[')
          count2--;
      }
      if(count1<0 || count2<0) {
        found_iter=iter;
        return true;
      }
    }
  }
  
  return false;
}  

bool Source::View::find_right_bracket_forward(Gtk::TextIter iter, Gtk::TextIter &found_iter) {
  int count=0;
  
  bool ignore=false;
  
  while(iter.forward_char()) {
    if(!get_source_buffer()->iter_has_context_class(iter, "comment") && !get_source_buffer()->iter_has_context_class(iter, "string")) {
      if(*iter=='\'') {
        auto before_iter=iter;
        before_iter.backward_char();
        auto before_before_iter=before_iter;
        before_before_iter.backward_char();
        if(*before_iter!='\\' || *before_before_iter=='\\')
          ignore=!ignore;
      }
      else if(!ignore) {
        if(*iter=='}') {
          if(count==0) {
            found_iter=iter;
            return true;
          }
          count--;
        }
        else if(*iter=='{')
          count++;
      }
    }
  }
  return false;
}

//Basic indentation
bool Source::View::on_key_press_event(GdkEventKey* key) {
  if(spellcheck_suggestions_dialog_shown) {
    if(spellcheck_suggestions_dialog->on_key_press(key))
      return true;
  }

  if(key->keyval==GDK_KEY_BackSpace)
    last_keyval_is_backspace=true;
  else
    last_keyval_is_backspace=false;
  
  get_source_buffer()->begin_user_action();
  //Indent as in next or previous line
  if(key->keyval==GDK_KEY_Return && !get_buffer()->get_has_selection()) {
    auto insert_it=get_buffer()->get_insert()->get_iter();
    int line_nr=insert_it.get_line();
    auto line=get_line_before();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex)) {
      if((line_nr+1)<get_buffer()->get_line_count()) {
        string next_line=get_line(line_nr+1);
        auto line_end_iter=get_buffer()->get_iter_at_line(line_nr+1);
        if(line_end_iter.backward_char()) {
          std::smatch sm2;
          if(insert_it==line_end_iter && std::regex_match(next_line, sm2, tabs_regex)) {
            if(sm2[1].str().size()>sm[1].str().size()) {
              get_source_buffer()->insert_at_cursor("\n"+sm2[1].str());
              scroll_to(get_source_buffer()->get_insert());
              get_source_buffer()->end_user_action();
              return true;
            }
          }
        }
      }
      get_source_buffer()->insert_at_cursor("\n"+sm[1].str());
      scroll_to(get_source_buffer()->get_insert());
      get_source_buffer()->end_user_action();
      return true;
    }
  }
  //Indent right when clicking tab, no matter where in the line the cursor is. Also works on selected text.
  else if(key->keyval==GDK_KEY_Tab) {
    Gtk::TextIter selection_start, selection_end;
    get_source_buffer()->get_selection_bounds(selection_start, selection_end);
    int line_start=selection_start.get_line();
    int line_end=selection_end.get_line();
    for(int line=line_start;line<=line_end;line++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line);
      if(!get_buffer()->get_has_selection() || line_it!=selection_end)
        get_source_buffer()->insert(line_it, tab);
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //Indent left when clicking shift-tab, no matter where in the line the cursor is. Also works on selected text.
  else if((key->keyval==GDK_KEY_ISO_Left_Tab || key->keyval==GDK_KEY_Tab) && (key->state&GDK_SHIFT_MASK)>0) {
    Gtk::TextIter selection_start, selection_end;
    get_source_buffer()->get_selection_bounds(selection_start, selection_end);
    int line_start=selection_start.get_line();
    int line_end=selection_end.get_line();
    
    unsigned indent_left_steps=tab_size;
    std::vector<bool> ignore_line;
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      auto line_it = get_source_buffer()->get_iter_at_line(line_nr);
      if(!get_buffer()->get_has_selection() || line_it!=selection_end) {
        string line=get_line(line_nr);
        std::smatch sm;
        if(std::regex_match(line, sm, tabs_regex) && (sm[1].str().size()>0 || sm[2].str().size()==0)) {
          if(sm[2].str().size()>0) {
            indent_left_steps=std::min(indent_left_steps, (unsigned)sm[1].str().size());
            ignore_line.push_back(false);
          }
          else if((unsigned)sm[1].str().size()<indent_left_steps)
            ignore_line.push_back(true);
          else
            ignore_line.push_back(false);
        }
        else {
          get_source_buffer()->end_user_action();
          return true;
        }
      }
    }
    
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line_nr);
      Gtk::TextIter line_plus_it=line_it;
      if(!get_buffer()->get_has_selection() || line_it!=selection_end) {
        if(indent_left_steps==0 || line_plus_it.forward_chars(indent_left_steps))
          if(!ignore_line.at(line_nr-line_start))
            get_source_buffer()->erase(line_it, line_plus_it);
      }
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //"Smart" backspace key TODO: remove commented out lines if this works
  else if(key->keyval==GDK_KEY_BackSpace && !get_buffer()->get_has_selection()) {
    auto insert_it=get_buffer()->get_insert()->get_iter();
    //int line_nr=insert_it.get_line();
    auto line=get_line_before();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()==line.size()) {
      auto line_start_iter=insert_it;
      if(line_start_iter.backward_chars(line.size()))
        get_buffer()->erase(insert_it, line_start_iter);
    }
  }

  bool stop=Gsv::View::on_key_press_event(key);
  get_source_buffer()->end_user_action();
  return stop;
}

bool Source::View::on_button_press_event(GdkEventButton *event) {
  if(event->type==GDK_2BUTTON_PRESS) {
    Gtk::TextIter start, end;
    get_buffer()->get_selection_bounds(start, end);
    auto iter=start;
    while((*iter>=48 && *iter<=57) || (*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter==95) {
      start=iter;
      if(!iter.backward_char())
        break;
    }
    while((*end>=48 && *end<=57) || (*end>=65 && *end<=90) || (*end>=97 && *end<=122) || *end==95) {
      if(!end.forward_char())
        break;
    }
    get_buffer()->select_range(start, end);
    return true;
  }
  
  return Gsv::View::on_button_press_event(event);
}

std::pair<char, unsigned> Source::View::find_tab_char_and_size() {
  const std::regex indent_regex("^([ \t]+)(.*)$");
  auto size=get_buffer()->get_line_count();
  std::unordered_map<char, size_t> tab_chars;
  std::unordered_map<unsigned, size_t> tab_sizes;
  unsigned last_tab_size=0;
  for(int c=0;c<size;c++) {
    auto line=get_line(c);
    std::smatch sm;
    std::string str;
    if(std::regex_match(line, sm, indent_regex)) {
      if(sm[2].str().size()==0)
        continue;
      str=sm[1].str();
    }
    else {
      str="";
      if(line.size()==0)
        continue;
    }
    
    long tab_diff=abs(static_cast<long>(str.size()-last_tab_size));
    if(tab_diff>0) {
      unsigned tab_diff_unsigned=static_cast<unsigned>(tab_diff);
      auto it_size=tab_sizes.find(tab_diff_unsigned);
      if(it_size!=tab_sizes.end())
        it_size->second++;
      else
        tab_sizes[tab_diff_unsigned]=1;
    }
    
    last_tab_size=str.size();
    
    if(str.size()>0) {
      auto it_char=tab_chars.find(str[0]);
      if(it_char!=tab_chars.end())
        it_char->second++;
      else
        tab_chars[str[0]]=1;
    }
  }
  char found_tab_char=0;
  size_t occurences=0;
  for(auto &tab_char: tab_chars) {
    if(tab_char.second>occurences) {
      found_tab_char=tab_char.first;
      occurences=tab_char.second;
    }
  }
  unsigned found_tab_size=0;
  occurences=0;
  for(auto &tab_size: tab_sizes) {
    if(tab_size.second>occurences) {
      found_tab_size=tab_size.first;
      occurences=tab_size.second;
    }
  }
  return {found_tab_char, found_tab_size};
}

bool Source::View::is_word_iter(const Gtk::TextIter& iter) {
  return ((*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter==39 || *iter>=128);
}

std::pair<Gtk::TextIter, Gtk::TextIter> Source::View::spellcheck_get_word(Gtk::TextIter iter) {
  auto start=iter;
  auto end=iter;
  
  while(is_word_iter(iter)) {
    start=iter;
{'fraction_non_alphanumeric': 0.14882447954055994, 'fraction_numerical': 0.005234505862646567, 'mean_word_length': 3.9854607813898, 'pattern_counts': {'":': 2, '<': 114, '<?xml version=': 0, '>': 510, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4574156', 'n_tokens_mistral': 22556, 'n_tokens_neox': 22301, 'n_words': 2917}