From 662b31785be0218df03d272f487588f0f08bd049 Mon Sep 17 00:00:00 2001 From: eidheim Date: Sun, 4 Feb 2018 10:22:59 +0100 Subject: [PATCH] Fixes #361: Added support for multiple cursors --- README.md | 1 + src/source.cc | 350 ++++++++++++++++++++++++++++++++++++++++++++------ src/source.h | 16 +++ 3 files changed, 325 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d0d59fd..ec01428 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ See [language-server-protocol/specification.md](https://github.com/Microsoft/lan * Run shell commands within juCi++ * Regex search and replace * Smart paste, keys and indentation +* Multiple cursors * Auto-indentation of C++ file buffers through [clang-format](http://clang.llvm.org/docs/ClangFormat.html) * Source minimap * Split view diff --git a/src/source.cc b/src/source.cc index b8e4914..d3fb26f 100644 --- a/src/source.cc +++ b/src/source.cc @@ -1071,6 +1071,14 @@ void Source::View::replace_all(const std::string &replacement) { } void Source::View::paste() { + class Guard { + public: + bool &value; + Guard(bool &value_) : value(value_) { value = true; } + ~Guard() { value = false; } + }; + Guard guard{multiple_cursors_use}; + std::string text=Gtk::Clipboard::get()->wait_for_text(); //Replace carriage returns (which leads to crash) with newlines @@ -1600,6 +1608,14 @@ void Source::View::cleanup_whitespace_characters_on_return(const Gtk::TextIter & } bool Source::View::on_key_press_event(GdkEventKey* key) { + class Guard { + public: + bool &value; + Guard(bool &value_) : value(value_) { value = true; } + ~Guard() { value = false; } + }; + Guard guard{multiple_cursors_use}; + if(SelectionDialog::get() && SelectionDialog::get()->is_visible()) { if(SelectionDialog::get()->on_key_press(key)) return true; @@ -1613,6 +1629,9 @@ bool Source::View::on_key_press_event(GdkEventKey* key) { previous_non_modifier_keyval=last_keyval; last_keyval=key->keyval; + if(on_key_press_event_multiple_cursors(key)) + return true; + //Move cursor one paragraph down if((key->keyval==GDK_KEY_Down || key->keyval==GDK_KEY_KP_Down) && (key->state&GDK_CONTROL_MASK)>0) { auto selection_start_iter=get_buffer()->get_selection_bound()->get_iter(); @@ -1678,7 +1697,7 @@ bool Source::View::on_key_press_event(GdkEventKey* key) { scroll_to(get_buffer()->get_insert()); return true; } - + get_buffer()->begin_user_action(); // Shift+enter: go to end of line and enter @@ -1713,6 +1732,33 @@ bool Source::View::on_key_press_event(GdkEventKey* key) { } } +Gtk::TextIter Source::View::get_smart_home_iter(const Gtk::TextIter &iter) { + auto start_line_iter=get_buffer()->get_iter_at_line(iter.get_line()); + auto start_sentence_iter=start_line_iter; + while(!start_sentence_iter.ends_line() && + (*start_sentence_iter==' ' || *start_sentence_iter=='\t') && + start_sentence_iter.forward_char()) {} + + if(iter>start_sentence_iter || iter==start_line_iter) + return start_sentence_iter; + else + return start_line_iter; +} +Gtk::TextIter Source::View::get_smart_end_iter(const Gtk::TextIter &iter) { + auto end_line_iter=get_iter_at_line_end(iter.get_line()); + auto end_sentence_iter=end_line_iter; + while(!end_sentence_iter.starts_line() && + (*end_sentence_iter==' ' || *end_sentence_iter=='\t' || end_sentence_iter.ends_line()) && + end_sentence_iter.backward_char()) {} + if(!end_sentence_iter.ends_line() && *end_sentence_iter!=' ' && *end_sentence_iter!='\t') + end_sentence_iter.forward_char(); + + if(iter==end_line_iter) + return end_sentence_iter; + else + return end_line_iter; +} + //Basic indentation bool Source::View::on_key_press_event_basic(GdkEventKey* key) { auto iter=get_buffer()->get_insert()->get_iter(); @@ -1868,50 +1914,20 @@ bool Source::View::on_key_press_event_basic(GdkEventKey* key) { return true; } } - //Next two are smart home/end keys that works with wrapped lines - //Note that smart end goes FIRST to end of line to avoid hiding empty chars after expressions - else if((key->keyval==GDK_KEY_End || key->keyval==GDK_KEY_KP_End) && (key->state&GDK_CONTROL_MASK)==0) { - auto end_line_iter=get_iter_at_line_end(iter.get_line()); - auto end_sentence_iter=end_line_iter; - while(!end_sentence_iter.starts_line() && - (*end_sentence_iter==' ' || *end_sentence_iter=='\t' || end_sentence_iter.ends_line()) && - end_sentence_iter.backward_char()) {} - if(!end_sentence_iter.ends_line() && *end_sentence_iter!=' ' && *end_sentence_iter!='\t') - end_sentence_iter.forward_char(); - if(iter==end_line_iter) { - if((key->state&GDK_SHIFT_MASK)>0) - get_buffer()->move_mark_by_name("insert", end_sentence_iter); - else - get_buffer()->place_cursor(end_sentence_iter); - } - else { - if((key->state&GDK_SHIFT_MASK)>0) - get_buffer()->move_mark_by_name("insert", end_line_iter); - else - get_buffer()->place_cursor(end_line_iter); - } + // Smart Home/End-keys + else if((key->keyval==GDK_KEY_Home || key->keyval==GDK_KEY_KP_Home) && (key->state&GDK_CONTROL_MASK)==0) { + if((key->state&GDK_SHIFT_MASK)>0) + get_buffer()->move_mark_by_name("insert", get_smart_home_iter(iter)); + else + get_buffer()->place_cursor(get_smart_home_iter(iter)); scroll_to(get_buffer()->get_insert()); return true; } - else if((key->keyval==GDK_KEY_Home || key->keyval==GDK_KEY_KP_Home) && (key->state&GDK_CONTROL_MASK)==0) { - auto start_line_iter=get_buffer()->get_iter_at_line(iter.get_line()); - auto start_sentence_iter=start_line_iter; - while(!start_sentence_iter.ends_line() && - (*start_sentence_iter==' ' || *start_sentence_iter=='\t') && - start_sentence_iter.forward_char()) {} - - if(iter>start_sentence_iter || iter==start_line_iter) { - if((key->state&GDK_SHIFT_MASK)>0) - get_buffer()->move_mark_by_name("insert", start_sentence_iter); - else - get_buffer()->place_cursor(start_sentence_iter); - } - else { - if((key->state&GDK_SHIFT_MASK)>0) - get_buffer()->move_mark_by_name("insert", start_line_iter); - else - get_buffer()->place_cursor(start_line_iter); - } + else if((key->keyval==GDK_KEY_End || key->keyval==GDK_KEY_KP_End) && (key->state&GDK_CONTROL_MASK)==0) { + if((key->state&GDK_SHIFT_MASK)>0) + get_buffer()->move_mark_by_name("insert", get_smart_end_iter(iter)); + else + get_buffer()->place_cursor(get_smart_end_iter(iter)); scroll_to(get_buffer()->get_insert()); return true; } @@ -2528,6 +2544,256 @@ bool Source::View::on_key_press_event_smart_inserts(GdkEventKey *key) { return false; } +bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { + if(!multiple_cursors_signals_set) { + multiple_cursors_signals_set=true; + multiple_cursors_last_insert=get_buffer()->create_mark(get_buffer()->get_insert()->get_iter(), false); + get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator &iter, const Glib::RefPtr &mark) { + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + if(extra_cursor.first==mark && (!iter.ends_line() || iter.get_line_offset()>extra_cursor.second)) { + extra_cursor.second=iter.get_line_offset(); + break; + } + } + + if(mark->get_name()=="insert") { + if(multiple_cursors_use) { + multiple_cursors_use=false; + auto offset_diff=mark->get_iter().get_offset()-multiple_cursors_last_insert->get_iter().get_offset(); + if(offset_diff!=0) { + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + iter.forward_chars(offset_diff); + get_buffer()->move_mark(extra_cursor.first, iter); + } + } + multiple_cursors_use=true; + } + get_buffer()->delete_mark(multiple_cursors_last_insert); + multiple_cursors_last_insert=get_buffer()->create_mark(mark->get_iter(), false); + } + }); + + // TODO: this handler should run after signal_insert + get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &iter, const Glib::ustring &text, int bytes) { + if(multiple_cursors_use) { + multiple_cursors_use=false; + auto offset=iter.get_offset()-get_buffer()->get_insert()->get_iter().get_offset(); + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + iter.forward_chars(offset); + get_buffer()->insert(iter, text); + } + multiple_cursors_use=true; + } + }); + + get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &iter_start, const Gtk::TextBuffer::iterator &iter_end) { + if(multiple_cursors_use) { + auto insert_offset=get_buffer()->get_insert()->get_iter().get_offset(); + multiple_cursors_erase_backward_length=insert_offset-iter_start.get_offset(); + multiple_cursors_erase_forward_length=iter_end.get_offset()-insert_offset; + } + }, false); + + get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &iter_start, const Gtk::TextBuffer::iterator &iter_end) { + if(multiple_cursors_use) { + multiple_cursors_use=false; + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto start_iter=extra_cursor.first->get_iter(); + auto end_iter=start_iter; + start_iter.backward_chars(multiple_cursors_erase_backward_length); + end_iter.forward_chars(multiple_cursors_erase_forward_length); + get_buffer()->erase(start_iter, end_iter); + } + multiple_cursors_use=true; + } + }); + } + + + if(key->keyval==GDK_KEY_Escape && !multiple_cursors_extra_cursors.empty()) { + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + extra_cursor.first->set_visible(false); + get_buffer()->delete_mark(extra_cursor.first); + } + multiple_cursors_extra_cursors.clear(); + return true; + } + + unsigned create_cursor_mask=GDK_MOD1_MASK; + unsigned move_last_created_cursor_mask=GDK_SHIFT_MASK|GDK_MOD1_MASK; + + // Move last created cursor + if((key->keyval==GDK_KEY_Left || key->keyval==GDK_KEY_KP_Left) && (key->state&move_last_created_cursor_mask)==move_last_created_cursor_mask) { + if(multiple_cursors_extra_cursors.empty()) + return false; + auto &cursor=multiple_cursors_extra_cursors.back().first; + auto iter=cursor->get_iter(); + iter.backward_char(); + get_buffer()->move_mark(cursor, iter); + return true; + } + if((key->keyval==GDK_KEY_Right || key->keyval==GDK_KEY_KP_Right) && (key->state&move_last_created_cursor_mask)==move_last_created_cursor_mask) { + if(multiple_cursors_extra_cursors.empty()) + return false; + auto &cursor=multiple_cursors_extra_cursors.back().first; + auto iter=cursor->get_iter(); + iter.forward_char(); + get_buffer()->move_mark(cursor, iter); + return true; + } + if((key->keyval==GDK_KEY_Up || key->keyval==GDK_KEY_KP_Up) && (key->state&move_last_created_cursor_mask)==move_last_created_cursor_mask) { + if(multiple_cursors_extra_cursors.empty()) + return false; + auto &extra_cursor=multiple_cursors_extra_cursors.back(); + auto iter=extra_cursor.first->get_iter(); + auto line_offset=extra_cursor.second; + if(iter.backward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(line_offset, end_line_iter.get_line_offset())); + get_buffer()->move_mark(extra_cursor.first, iter); + } + return true; + } + if((key->keyval==GDK_KEY_Down || key->keyval==GDK_KEY_KP_Down) && (key->state&move_last_created_cursor_mask)==move_last_created_cursor_mask) { + if(multiple_cursors_extra_cursors.empty()) + return false; + auto &extra_cursor=multiple_cursors_extra_cursors.back(); + auto iter=extra_cursor.first->get_iter(); + auto line_offset=extra_cursor.second; + if(iter.forward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(line_offset, end_line_iter.get_line_offset())); + get_buffer()->move_mark(extra_cursor.first, iter); + } + return true; + } + + // Create extra cursor + if((key->keyval==GDK_KEY_Up || key->keyval==GDK_KEY_KP_Up) && (key->state&create_cursor_mask)==create_cursor_mask) { + auto insert_iter=get_buffer()->get_insert()->get_iter(); + auto insert_line_offset=insert_iter.get_line_offset(); + auto offset=insert_iter.get_offset(); + for(auto &extra_cursor: multiple_cursors_extra_cursors) + offset=std::min(offset, extra_cursor.first->get_iter().get_offset()); + auto iter=get_buffer()->get_iter_at_offset(offset); + if(iter.backward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(insert_line_offset, end_line_iter.get_line_offset())); + multiple_cursors_extra_cursors.emplace_back(get_buffer()->create_mark(iter, false), insert_line_offset); + multiple_cursors_extra_cursors.back().first->set_visible(true); + } + return true; + } + if((key->keyval==GDK_KEY_Down || key->keyval==GDK_KEY_KP_Down) && (key->state&create_cursor_mask)==create_cursor_mask) { + auto insert_iter=get_buffer()->get_insert()->get_iter(); + auto insert_line_offset=insert_iter.get_line_offset(); + auto offset=insert_iter.get_offset(); + for(auto &extra_cursor: multiple_cursors_extra_cursors) + offset=std::max(offset, extra_cursor.first->get_iter().get_offset()); + auto iter=get_buffer()->get_iter_at_offset(offset); + if(iter.forward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(insert_line_offset, end_line_iter.get_line_offset())); + multiple_cursors_extra_cursors.emplace_back(get_buffer()->create_mark(iter, false), insert_line_offset); + multiple_cursors_extra_cursors.back().first->set_visible(true); + } + return true; + } + + // Move cursors left/right + if((key->keyval==GDK_KEY_Left || key->keyval==GDK_KEY_KP_Left) && (key->state&GDK_CONTROL_MASK)>0) { + multiple_cursors_use=false; + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + iter.backward_word_start(); + extra_cursor.second=iter.get_line_offset(); + get_buffer()->move_mark(extra_cursor.first, iter); + } + auto insert=get_buffer()->get_insert(); + auto iter=insert->get_iter(); + iter.backward_word_start(); + get_buffer()->move_mark(insert, iter); + if((key->state&GDK_SHIFT_MASK)==0) + get_buffer()->move_mark_by_name("selection_bound", iter); + return true; + } + if((key->keyval==GDK_KEY_Right || key->keyval==GDK_KEY_KP_Right) && (key->state&GDK_CONTROL_MASK)>0) { + multiple_cursors_use=false; + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + iter.forward_visible_word_end(); + extra_cursor.second=iter.get_line_offset(); + get_buffer()->move_mark(extra_cursor.first, iter); + } + auto insert=get_buffer()->get_insert(); + auto iter=insert->get_iter(); + iter.forward_visible_word_end(); + get_buffer()->move_mark(insert, iter); + if((key->state&GDK_SHIFT_MASK)==0) + get_buffer()->move_mark_by_name("selection_bound", iter); + return true; + } + + // Move cursors up/down + if((key->keyval==GDK_KEY_Up || key->keyval==GDK_KEY_KP_Up)) { + multiple_cursors_use=false; + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + auto line_offset=extra_cursor.second; + if(iter.backward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(line_offset, end_line_iter.get_line_offset())); + get_buffer()->move_mark(extra_cursor.first, iter); + } + } + return false; + } + if((key->keyval==GDK_KEY_Down || key->keyval==GDK_KEY_KP_Down)) { + multiple_cursors_use=false; + for(auto &extra_cursor: multiple_cursors_extra_cursors) { + auto iter=extra_cursor.first->get_iter(); + auto line_offset=extra_cursor.second; + if(iter.forward_line()) { + auto end_line_iter=iter; + if(!end_line_iter.ends_line()) + end_line_iter.forward_to_line_end(); + iter.forward_chars(std::min(line_offset, end_line_iter.get_line_offset())); + get_buffer()->move_mark(extra_cursor.first, iter); + } + } + return false; + } + + // Smart Home-key, start of line + if((key->keyval==GDK_KEY_Home || key->keyval==GDK_KEY_KP_Home) && (key->state&GDK_CONTROL_MASK)==0) { + for(auto &extra_cursor: multiple_cursors_extra_cursors) + get_buffer()->move_mark(extra_cursor.first, get_smart_home_iter(extra_cursor.first->get_iter())); + multiple_cursors_use=false; + return false; + } + // Smart End-key, end of line + if((key->keyval==GDK_KEY_End || key->keyval==GDK_KEY_KP_End) && (key->state&GDK_CONTROL_MASK)==0) { + for(auto &extra_cursor: multiple_cursors_extra_cursors) + get_buffer()->move_mark(extra_cursor.first, get_smart_end_iter(extra_cursor.first->get_iter())); + multiple_cursors_use=false; + return false; + } + + return false; +} + bool Source::View::on_button_press_event(GdkEventButton *event) { // Select range when double clicking if(event->type==GDK_2BUTTON_PRESS) { diff --git a/src/source.h b/src/source.h index 1bca74b..e8e5dd0 100644 --- a/src/source.h +++ b/src/source.h @@ -149,6 +149,14 @@ namespace Source { void cleanup_whitespace_characters_on_return(const Gtk::TextIter &iter); + /// Move iter to line start. Depending on iter position, before or after indentation. + /// Works with wrapped lines. + Gtk::TextIter get_smart_home_iter(const Gtk::TextIter &iter); + /// Move iter to line end. Depending on iter position, before or after indentation. + /// Works with wrapped lines. + /// Note that smart end goes FIRST to end of line to avoid hiding empty chars after expressions. + Gtk::TextIter get_smart_end_iter(const Gtk::TextIter &iter); + bool is_bracket_language=false; bool on_key_press_event(GdkEventKey* key) override; bool on_key_press_event_basic(GdkEventKey* key); @@ -175,6 +183,14 @@ namespace Source { static void search_occurrences_updated(GtkWidget* widget, GParamSpec* property, gpointer data); sigc::connection renderer_activate_connection; + + bool multiple_cursors_signals_set=false; + bool multiple_cursors_use=false; + std::vector, int>> multiple_cursors_extra_cursors; + Glib::RefPtr multiple_cursors_last_insert; + int multiple_cursors_erase_backward_length; + int multiple_cursors_erase_forward_length; + bool on_key_press_event_multiple_cursors(GdkEventKey* key); }; class GenericView : public View {