<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 {
  SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
}

AspellConfig* spellcheck_config=NULL;

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 ////
//////////////
Source::View::View(const boost::filesystem::path &file_path): file_path(file_path) {
  set_smart_home_end(Gsv::SMART_HOME_END_BEFORE);
  get_source_buffer()->begin_not_undoable_action();
  if(!juci::filesystem::read(file_path, get_buffer()))
    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');
  }
  
  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();
    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](){
      auto iter=get_buffer()->get_insert()->get_iter();
      if(iter.backward_char()) {
        auto context_iter=iter;
        if(context_iter.backward_char()) {
          if(get_source_buffer()->iter_has_context_class(context_iter, "comment") || get_source_buffer()->iter_has_context_class(context_iter, "string")) {
            if(*iter==32) { //Might have used space to split two words
              auto first=iter;
              auto second=iter;
              if(first.backward_char() && second.forward_char()) {
                get_buffer()->remove_tag_by_name("spellcheck_error", first, second);
                spellcheck(first);
                spellcheck(second);
              }
            }
            else
              spellcheck(iter);
          }
        }
      }
    });
  }
  
  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==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);
          }
          spellcheck_tooltips.show(rectangle);
          return false;
        }, 100);
      }
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      spellcheck_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);
        }
        spellcheck_tooltips.show(rectangle);
        return false;
      }, 500);
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      spellcheck_tooltips.hide();
    }
  });
  
  signal_scroll_event().connect([this](GdkEventScroll* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    spellcheck_tooltips.hide();
    return false;
  });
  
  signal_focus_out_event().connect([this](GdkEventFocus* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    spellcheck_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);
  
  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() {
  Gtk::Clipboard::get()->request_text([this](const Glib::ustring& text){
    auto line=get_line_before_insert();
    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) {
          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
              break;
          }
          if(first_paste_line) {
            if(tabs!=0) {
              first_paste_line_has_tabs=true;
              paste_line_tabs=tabs;
            }
            first_paste_line=false;
          }
          else
            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) {
          if(first_paste_line) {
            if(first_paste_line_has_tabs)
              get_buffer()->insert_at_cursor(text.substr(start_line+paste_line_tabs, end_line-start_line-paste_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+paste_line_tabs, end_line-start_line-paste_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
      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);
}

string Source::View::get_line(size_t line_number) {
  Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line_number);
  Gtk::TextIter line_end_it = line_it;
  while(!line_end_it.ends_line())
    line_end_it++;
  std::string line(get_source_buffer()->get_text(line_it, line_end_it));
  return line;
}

string Source::View::get_line_before_insert() {
  Gtk::TextIter insert_it = get_source_buffer()->get_insert()->get_iter();
  Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(insert_it.get_line());
  std::string line(get_source_buffer()->get_text(line_it, insert_it));
  return line;
}

//Basic indentation
bool Source::View::on_key_press_event(GdkEventKey* key) {
  get_source_buffer()->begin_user_action();
  //Indent as in next or previous line
  if(key->keyval==GDK_KEY_Return && key->state==0 && !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_insert();
    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);
        line_end_iter--;
        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 && key->state==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();
    for(int line=line_start;line<=line_end;line++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line);
      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) {
    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;
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      string line=get_line(line_nr);
      std::smatch sm;
      if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()>0) {
        indent_left_steps=std::min(indent_left_steps, (unsigned)sm[1].str().size());
      }
      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;
      
      for(unsigned c=0;c<indent_left_steps;c++)
        line_plus_it++;
      get_source_buffer()->erase(line_it, line_plus_it);
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //"Smart" backspace key
  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_insert();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()==line.size()) {
      if((line_nr-1)>=0) {
        string previous_line=get_line(line_nr-1);
        std::smatch sm2;
        if(std::regex_match(previous_line, sm2, tabs_regex)) {
          if(line.size()==sm2[1].str().size() || line.size()==sm2[1].str().size()+tab_size || line.size()==sm2[1].str().size()-tab_size) {
            auto previous_line_end_it=insert_it;
            for(unsigned c=0;c<line.size();c++)
              previous_line_end_it--;
            previous_line_end_it--;
            get_source_buffer()->erase(previous_line_end_it, insert_it);
            get_source_buffer()->end_user_action();
            return true;
          }
        }
      }
      if(line.size()>=tab_size) {
        auto insert_minus_tab_it=insert_it;
        for(unsigned c=0;c<tab_size;c++)
          insert_minus_tab_it--;
        get_source_buffer()->erase(insert_minus_tab_it, insert_it);
        get_source_buffer()->end_user_action();
        return true;
      }
    }
  }

  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;
    if(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;
    if(std::regex_match(line, sm, indent_regex)) {
      auto str=sm[1].str();
      
      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};
}

void Source::View::spellcheck(Gtk::TextIter iter) {
  auto start=iter;
  auto end=iter;
  
  while((*iter>=48 && *iter<=57) || (*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter>=128) {
    start=iter;
    if(!iter.backward_char())
      break;
  }
  while((*end>=48 && *end<=57) || (*end>=65 && *end<=90) || (*end>=97 && *end<=122) || *end>=128) {
    if(!end.forward_char())
      break;
  }
  for(auto it=spellcheck_tooltips.begin();it!=spellcheck_tooltips.end();it++) {
    if(it->start_mark->get_iter()==start || it->start_mark->get_iter()==it->end_mark->get_iter())
      it=spellcheck_tooltips.erase(it);
  }
  auto word=get_buffer()->get_text(start, end);
  std::vector<Glib::ustring> words;
  if(word.size()>0) {
    auto correct = aspell_speller_check(spellcheck_checker, word.data(), word.bytes());
    if(correct==0) {
      get_buffer()->apply_tag_by_name("spellcheck_error", start, end);
      
      const AspellWordList *suggestions = aspell_speller_suggest(spellcheck_checker, word.data(), word.bytes());
      AspellStringEnumeration *elements = aspell_word_list_elements(suggestions);
      
      auto words=std::make_shared<std::string>();
      const char *word;
      while ((word = aspell_string_enumeration_next(elements))!= NULL) {
        if(words->size()==0)
          *words="Suggestions:";
        (*words)+=std::string("\n")+word;
      }
      delete_aspell_string_enumeration(elements);
      
      if(words->size()>0) {
        auto create_tooltip_buffer=[this, words]() {
          auto tooltip_buffer=Gtk::TextBuffer::create(get_buffer()->get_tag_table());
          tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), *words, "def:note");
          return tooltip_buffer;
        };
        spellcheck_tooltips.emplace_back(create_tooltip_buffer, *this, get_buffer()->create_mark(start), get_buffer()->create_mark(end));
      }
    }<commit_msg>Spellcheck suggestions now separated with ', ' instead of newline.<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 {
  SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE
}

AspellConfig* spellcheck_config=NULL;

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 ////
//////////////
Source::View::View(const boost::filesystem::path &file_path): file_path(file_path) {
  set_smart_home_end(Gsv::SMART_HOME_END_BEFORE);
  get_source_buffer()->begin_not_undoable_action();
  if(!juci::filesystem::read(file_path, get_buffer()))
    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');
  }
  
  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();
    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](){
      auto iter=get_buffer()->get_insert()->get_iter();
      if(iter.backward_char()) {
        auto context_iter=iter;
        if(context_iter.backward_char()) {
          if(get_source_buffer()->iter_has_context_class(context_iter, "comment") || get_source_buffer()->iter_has_context_class(context_iter, "string")) {
            if(*iter==32) { //Might have used space to split two words
              auto first=iter;
              auto second=iter;
              if(first.backward_char() && second.forward_char()) {
                get_buffer()->remove_tag_by_name("spellcheck_error", first, second);
                spellcheck(first);
                spellcheck(second);
              }
            }
            else
              spellcheck(iter);
          }
        }
      }
    });
  }
  
  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==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);
          }
          spellcheck_tooltips.show(rectangle);
          return false;
        }, 100);
      }
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      spellcheck_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);
        }
        spellcheck_tooltips.show(rectangle);
        return false;
      }, 500);
      type_tooltips.hide();
      diagnostic_tooltips.hide();
      spellcheck_tooltips.hide();
    }
  });
  
  signal_scroll_event().connect([this](GdkEventScroll* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    spellcheck_tooltips.hide();
    return false;
  });
  
  signal_focus_out_event().connect([this](GdkEventFocus* event) {
    delayed_tooltips_connection.disconnect();
    type_tooltips.hide();
    diagnostic_tooltips.hide();
    spellcheck_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);
  
  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() {
  Gtk::Clipboard::get()->request_text([this](const Glib::ustring& text){
    auto line=get_line_before_insert();
    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) {
          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
              break;
          }
          if(first_paste_line) {
            if(tabs!=0) {
              first_paste_line_has_tabs=true;
              paste_line_tabs=tabs;
            }
            first_paste_line=false;
          }
          else
            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) {
          if(first_paste_line) {
            if(first_paste_line_has_tabs)
              get_buffer()->insert_at_cursor(text.substr(start_line+paste_line_tabs, end_line-start_line-paste_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+paste_line_tabs, end_line-start_line-paste_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
      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);
}

string Source::View::get_line(size_t line_number) {
  Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line_number);
  Gtk::TextIter line_end_it = line_it;
  while(!line_end_it.ends_line())
    line_end_it++;
  std::string line(get_source_buffer()->get_text(line_it, line_end_it));
  return line;
}

string Source::View::get_line_before_insert() {
  Gtk::TextIter insert_it = get_source_buffer()->get_insert()->get_iter();
  Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(insert_it.get_line());
  std::string line(get_source_buffer()->get_text(line_it, insert_it));
  return line;
}

//Basic indentation
bool Source::View::on_key_press_event(GdkEventKey* key) {
  get_source_buffer()->begin_user_action();
  //Indent as in next or previous line
  if(key->keyval==GDK_KEY_Return && key->state==0 && !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_insert();
    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);
        line_end_iter--;
        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 && key->state==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();
    for(int line=line_start;line<=line_end;line++) {
      Gtk::TextIter line_it = get_source_buffer()->get_iter_at_line(line);
      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) {
    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;
    for(int line_nr=line_start;line_nr<=line_end;line_nr++) {
      string line=get_line(line_nr);
      std::smatch sm;
      if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()>0) {
        indent_left_steps=std::min(indent_left_steps, (unsigned)sm[1].str().size());
      }
      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;
      
      for(unsigned c=0;c<indent_left_steps;c++)
        line_plus_it++;
      get_source_buffer()->erase(line_it, line_plus_it);
    }
    get_source_buffer()->end_user_action();
    return true;
  }
  //"Smart" backspace key
  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_insert();
    std::smatch sm;
    if(std::regex_match(line, sm, tabs_regex) && sm[1].str().size()==line.size()) {
      if((line_nr-1)>=0) {
        string previous_line=get_line(line_nr-1);
        std::smatch sm2;
        if(std::regex_match(previous_line, sm2, tabs_regex)) {
          if(line.size()==sm2[1].str().size() || line.size()==sm2[1].str().size()+tab_size || line.size()==sm2[1].str().size()-tab_size) {
            auto previous_line_end_it=insert_it;
            for(unsigned c=0;c<line.size();c++)
              previous_line_end_it--;
            previous_line_end_it--;
            get_source_buffer()->erase(previous_line_end_it, insert_it);
            get_source_buffer()->end_user_action();
            return true;
          }
        }
      }
      if(line.size()>=tab_size) {
        auto insert_minus_tab_it=insert_it;
        for(unsigned c=0;c<tab_size;c++)
          insert_minus_tab_it--;
        get_source_buffer()->erase(insert_minus_tab_it, insert_it);
        get_source_buffer()->end_user_action();
        return true;
      }
    }
  }

  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;
    if(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;
    if(std::regex_match(line, sm, indent_regex)) {
      auto str=sm[1].str();
      
      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};
}

void Source::View::spellcheck(Gtk::TextIter iter) {
  auto start=iter;
  auto end=iter;
  
  while((*iter>=48 && *iter<=57) || (*iter>=65 && *iter<=90) || (*iter>=97 && *iter<=122) || *iter>=128) {
    start=iter;
    if(!iter.backward_char())
      break;
  }
  while((*end>=48 && *end<=57) || (*end>=65 && *end<=90) || (*end>=97 && *end<=122) || *end>=128) {
    if(!end.forward_char())
      break;
  }
  for(auto it=spellcheck_tooltips.begin();it!=spellcheck_tooltips.end();it++) {
    if(it->start_mark->get_iter()==start || it->start_mark->get_iter()==it->end_mark->get_iter())
      it=spellcheck_tooltips.erase(it);
  }
  auto word=get_buffer()->get_text(start, end);
  std::vector<Glib::ustring> words;
  if(word.size()>0) {
    auto correct = aspell_speller_check(spellcheck_checker, word.data(), word.bytes());
    if(correct==0) {
      get_buffer()->apply_tag_by_name("spellcheck_error", start, end);
      
      const AspellWordList *suggestions = aspell_speller_suggest(spellcheck_checker, word.data(), word.bytes());
      AspellStringEnumeration *elements = aspell_word_list_elements(suggestions);
      
      auto words=std::make_shared<std::string>();
      const char *word;
      while ((word = aspell_string_enumeration_next(elements))!= NULL) {
        if(words->size()==0) {
          *words="Suggestions:\n";
          (*words)+=word;
        }
        else
          (*words)+=std::string(", ")+word;
      }
      delete_aspell_string_enumeration(elements);
      
      if(words->size()>0) {
        auto create_tooltip_buffer=[this, words]() {
          auto tooltip_buffer=Gtk::TextBuffer::create(get_buffer()->get_tag_table());
          tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), *words, "def:note");
          return tooltip_buffer;
        };
        spellcheck_tooltips.emplace_back(create_tooltip_buffer, *this, get_buffer()->create_mark(start), get_buffer()->create_mark(end));
      }
    }
{'fraction_non_alphanumeric': 0.1479039007622179, 'fraction_numerical': 0.005940816021521447, 'mean_word_length': 4.178889318885449, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 451, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7268531', 'n_tokens_mistral': 18145, 'n_tokens_neox': 17905, 'n_words': 2308}