diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bd99f8..de8b275 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required (VERSION 2.8.8) project(juci) -set(JUCI_VERSION "1.4.6.5") +set(JUCI_VERSION "1.4.6.7") set(CPACK_PACKAGE_NAME "jucipp") set(CPACK_PACKAGE_CONTACT "Ole Christian Eidheim ") diff --git a/README.md b/README.md index 9e98760..87220db 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ towards libclang with speed, stability, and ease of use in mind. * Run shell commands within juCi++ * Regex search and replace * Smart paste, keys and indentation +* Extend/shrink selection * Multiple cursors * Snippets can be added in ~/.juci/snippets.json. The language ids used in the regexes can be found here: https://gitlab.gnome.org/GNOME/gtksourceview/tree/master/data/language-specs. * Auto-indentation through [clang-format](http://clang.llvm.org/docs/ClangFormat.html) or [Prettier](https://github.com/prettier/prettier) if installed diff --git a/src/files.h b/src/files.h index 8cad731..ea1c0bd 100644 --- a/src/files.h +++ b/src/files.h @@ -89,6 +89,8 @@ const std::string default_config_file = R"RAW({ "edit_cut": "x", "edit_copy": "c", "edit_paste": "v", + "edit_extend_selection": "a", + "edit_shrink_selection": "a", "edit_show_or_hide": "", "edit_find": "f", "source_spellcheck": "", diff --git a/src/menu.cc b/src/menu.cc index 8ef6832..de38028 100644 --- a/src/menu.cc +++ b/src/menu.cc @@ -197,6 +197,16 @@ const Glib::ustring menu_xml = R"RAW( app.edit_paste +
+ + _Extend _Selection + app.edit_extend_selection + + + _Shrink _Selection + app.edit_shrink_selection + +
_Show/_Hide diff --git a/src/source.cc b/src/source.cc index 883805e..bf12c33 100644 --- a/src/source.cc +++ b/src/source.cc @@ -463,6 +463,8 @@ void Source::View::setup_signals() { get_buffer()->remove_tag(clickable_tag, get_buffer()->begin(), get_buffer()->end()); clickable_tag_applied = false; } + + previous_extended_selections.clear(); }); @@ -558,10 +560,12 @@ void Source::View::setup_signals() { }); get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator &iterator, const Glib::RefPtr &mark) { - if(get_buffer()->get_has_selection() && mark->get_name() == "selection_bound") + auto mark_name = mark->get_name(); + + if(get_buffer()->get_has_selection() && mark_name == "selection_bound") delayed_tooltips_connection.disconnect(); - if(mark->get_name() == "insert") { + if(mark_name == "insert") { hide_tooltips(); delayed_tooltips_connection.disconnect(); @@ -595,7 +599,14 @@ void Source::View::setup_signals() { if(update_status_location) update_status_location(this); + + if(!keep_previous_extended_selections) + previous_extended_selections.clear(); } + + if(!keep_previous_extended_selections && (mark_name == "insert" || mark_name == "selection_bound")) + if(!keep_previous_extended_selections) + previous_extended_selections.clear(); }); signal_key_release_event().connect([this](GdkEventKey *event) { @@ -1024,6 +1035,534 @@ void Source::View::hide_dialogs() { CompletionDialog::get()->hide(); } +void Source::View::extend_selection() { + // Have tried to generalize this function as much as possible due to the complexity of this task, + // but some further workarounds for edge cases might be needed + + // It is impossible to identify <> used for templates by syntax alone, but + // this function works in most cases. + auto is_template_arguments = [this](Gtk::TextIter start, Gtk::TextIter end) { + if(*start != '<' || *end != '>' || start.get_line() != end.get_line()) + return false; + auto prev = start; + if(!prev.backward_char()) + return false; + if(!is_token_char(*prev)) + return false; + auto next = end; + next.forward_char(); + if(*next != '(' && *next != ' ') + return false; + return true; + }; + + // Extends expression from 'here' in for instance: test->here(...), test.test(here) or here.test(test) + auto extend_expression = [&](Gtk::TextIter &start, Gtk::TextIter &end) { + auto start_stored = start; + auto end_stored = end; + bool extend_token_forward = true, extend_token_backward = true; + + auto iter = start; + auto prev = iter; + if(prev.backward_char() && ((*prev == '(' && *end == ')') || (*prev == '[' && *end == ']') || (*prev == '<' && *end == '>') || (*prev == '{' && *end == '}'))) { + if(*prev == '<' && !is_template_arguments(prev, end)) + return false; + iter = start = prev; + end.forward_char(); + extend_token_forward = false; + } + else if(is_token_char(*iter)) { + auto token = get_token_iters(iter); + if(start != token.first || end != token.second) + return false; + extend_token_forward = false; + extend_token_backward = false; + } + else + return false; + + // Extend expression forward passed for instance member function + { + auto iter = end; + + bool extend_token = extend_token_forward; + while(forward_to_code(iter)) { + if(extend_token && is_token_char(*iter)) { + auto token = get_token_iters(iter); + iter = end = token.second; + extend_token = false; + continue; + } + + if(!extend_token && *iter == '(' && iter.forward_char() && find_close_symbol_forward(iter, iter, '(', ')')) { + iter.forward_char(); + end = iter; + extend_token = false; + continue; + } + if(!extend_token && *iter == '[' && iter.forward_char() && find_close_symbol_forward(iter, iter, '[', ']')) { + iter.forward_char(); + end = iter; + extend_token = false; + continue; + } + auto prev = iter; + if(!extend_token && *iter == '<' && iter.forward_char() && find_close_symbol_forward(iter, iter, '<', '>') && is_template_arguments(prev, iter)) { // Only extend for instance std::max(1, 2) + iter.forward_char(); + end = iter; + extend_token = false; + continue; + } + + if(!extend_token && *iter == '.') { + iter.forward_char(); + extend_token = true; + continue; + } + auto next = iter; + if(!extend_token && next.forward_char() && ((*iter == ':' && *next == ':') || (*iter == '-' && *next == '>'))) { + iter = next; + iter.forward_char(); + extend_token = true; + continue; + } + + break; + } + + // Extend through {} + auto prev = iter = end; + prev.backward_char(); + if(*prev != '}' && forward_to_code(iter) && *iter == '{' && iter.forward_char() && find_close_symbol_forward(iter, iter, '{', '}')) { + iter.forward_char(); + end = iter; + } + } + + // Extend backward + iter = start; + bool extend_token = extend_token_backward; + while(true) { + if(!iter.backward_char() || !backward_to_code(iter)) + break; + + if(extend_token && is_token_char(*iter)) { + auto token = get_token_iters(iter); + start = iter = token.first; + extend_token = false; + continue; + } + + if(extend_token && *iter == ')' && iter.backward_char() && find_open_symbol_backward(iter, iter, '(', ')')) { + start = iter; + extend_token = true; + continue; + } + if(extend_token && *iter == ']' && iter.backward_char() && find_open_symbol_backward(iter, iter, '[', ']')) { + start = iter; + extend_token = true; + continue; + } + auto angle_end = iter; + if(extend_token && *iter == '>' && iter.backward_char() && find_open_symbol_backward(iter, iter, '<', '>') && is_template_arguments(iter, angle_end)) { // Only extend for instance std::max(1, 2) + start = iter; + continue; + } + + if(*iter == '.') { + extend_token = true; + continue; + } + if(angle_end.backward_char() && ((*angle_end == ':' && *iter == ':') || (*angle_end == '-' && *iter == '>'))) { + iter = angle_end; + extend_token = true; + continue; + } + + break; + } + + if(start != start_stored || end != end_stored) + return true; + return false; + }; + + Gtk::TextIter start, end; + get_buffer()->get_selection_bounds(start, end); + auto start_stored = start; + auto end_stored = end; + + previous_extended_selections.emplace_back(start, end); + keep_previous_extended_selections = true; + ScopeGuard guard{[this] { + keep_previous_extended_selections = false; + }}; + + // Select token + if(!get_buffer()->get_has_selection()) { + auto iter = get_buffer()->get_insert()->get_iter(); + if(is_token_char(*iter)) { + auto token = get_token_iters(iter); + get_buffer()->select_range(token.first, token.second); + return; + } + } + + // Select string or comment block + auto before_start = start; + if(!is_code_iter(start) && !(before_start.backward_char() && is_code_iter(before_start) && is_code_iter(end))) { + bool no_code_iter = true; + for(auto iter = start; iter.forward_char() && iter < end;) { + if(is_code_iter(iter)) { + no_code_iter = false; + break; + } + } + if(no_code_iter) { + if(backward_to_code(start)) { + while(start.forward_char() && (*start == ' ' || *start == '\t' || start.ends_line())) { + } + } + if(forward_to_code(end)) { + while(end.backward_char() && (*end == ' ' || *end == '\t' || end.ends_line())) { + } + end.forward_char(); + } + if(start != start_stored || end != end_stored) { + get_buffer()->select_range(start, end); + return; + } + start = start_stored; + end = end_stored; + } + } + + // Select expression from token + if(get_buffer()->get_has_selection() && is_token_char(*start) && start.get_line() == end.get_line() && extend_expression(start, end)) { + get_buffer()->select_range(start, end); + return; + } + + before_start = start; + auto before_end = end; + bool ignore_comma = false; + auto start_sentence_iter = get_buffer()->end(); + auto end_sentence_iter = get_buffer()->end(); + if(is_code_iter(start) && is_code_iter(end) && before_start.backward_char() && before_end.backward_char()) { + if((*before_start == '(' && *end == ')') || + (*before_start == '[' && *end == ']') || + (*before_start == '<' && *end == '>') || + (*before_start == '{' && *end == '}')) { + // Select expression from selected brackets + if(extend_expression(start, end)) { + get_buffer()->select_range(start, end); + return; + } + start = before_start; + end.forward_char(); + } + else if((*before_start == ',' && *end == ',') || + (*before_start == ',' && *end == ')') || + (*before_start == ',' && *end == ']') || + (*before_start == ',' && *end == '>') || + (*before_start == ',' && *end == '}') || + (*before_start == '(' && *end == ',') || + (*before_start == '[' && *end == ',') || + (*before_start == '<' && *end == ',') || + (*before_start == '{' && *end == ',')) + ignore_comma = true; + else if(start != end && (*before_end == ';' || *before_end == '}')) { + auto iter = end; + if(*before_end == '}' && forward_to_code(iter) && *iter == ';') + end_sentence_iter = iter; + else + end_sentence_iter = before_end; + } + } + + int para_count = 0; + int square_count = 0; + int curly_count = 0; + auto start_comma_iter = get_buffer()->end(); + auto start_angle_iter = get_buffer()->end(); + while(start.backward_char()) { + if(*start == '(' && is_code_iter(start)) + para_count++; + else if(*start == ')' && is_code_iter(start)) + para_count--; + else if(*start == '[' && is_code_iter(start)) + square_count++; + else if(*start == ']' && is_code_iter(start)) + square_count--; + else if(*start == '{' && is_code_iter(start)) { + if(!start_sentence_iter && + para_count == 0 && square_count == 0 && curly_count == 0) { + start_sentence_iter = start; + } + curly_count++; + } + else if(*start == '}' && is_code_iter(start)) { + if(!start_sentence_iter && + para_count == 0 && square_count == 0 && curly_count == 0) { + auto next = start; + if(next.forward_char() && forward_to_code(next) && *next != ';') + start_sentence_iter = start; + } + curly_count--; + } + else if(!ignore_comma && !start_comma_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *start == ',' && is_code_iter(start)) + start_comma_iter = start; + else if(!start_sentence_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *start == ';' && is_code_iter(start)) + start_sentence_iter = start; + else if(!start_angle_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *start == '<' && is_code_iter(start)) + start_angle_iter = start; + if(*start == ';' && is_code_iter(start)) { + ignore_comma = true; + start_comma_iter = get_buffer()->end(); + } + if(para_count > 0 || square_count > 0 || curly_count > 0) + break; + } + + para_count = 0; + square_count = 0; + curly_count = 0; + auto end_comma_iter = get_buffer()->end(); + auto end_angle_iter = get_buffer()->end(); + do { + if(*end == '(' && is_code_iter(end)) + para_count++; + else if(*end == ')' && is_code_iter(end)) + para_count--; + else if(*end == '[' && is_code_iter(end)) + square_count++; + else if(*end == ']' && is_code_iter(end)) + square_count--; + else if(*end == '{' && is_code_iter(end)) + curly_count++; + else if(*end == '}' && is_code_iter(end)) { + curly_count--; + if(!end_sentence_iter && + para_count == 0 && square_count == 0 && curly_count == 0) { + auto next = end_sentence_iter = end; + if(next.forward_char() && forward_to_code(next) && *next == ';') + end_sentence_iter = next; + } + } + else if(!ignore_comma && !end_comma_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *end == ',' && is_code_iter(end)) + end_comma_iter = end; + else if(!end_sentence_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *end == ';' && is_code_iter(end)) + end_sentence_iter = end; + else if(!end_angle_iter && + para_count == 0 && square_count == 0 && curly_count == 0 && + *end == '>' && is_code_iter(end)) + end_angle_iter = end; + if(*end == ';' && is_code_iter(end)) { + ignore_comma = true; + start_comma_iter = get_buffer()->end(); + end_comma_iter = get_buffer()->end(); + } + if(para_count < 0 || square_count < 0 || curly_count < 0) + break; + } while(end.forward_char()); + + // Test for <> used for template arguments + if(start_angle_iter && end_angle_iter && is_template_arguments(start_angle_iter, end_angle_iter)) { + start = start_angle_iter; + end = end_angle_iter; + } + + // Test for matching brackets and try select regions within brackets separated by ',' + bool comma_used = false; + bool select_matching_brackets = false; + if((*start == '(' && *end == ')') || + (*start == '[' && *end == ']') || + (*start == '<' && *end == '>') || + (*start == '{' && *end == '}')) { + if(start_comma_iter && start < start_comma_iter) { + start = start_comma_iter; + comma_used = true; + } + if(end_comma_iter && end > end_comma_iter) { + end = end_comma_iter; + comma_used = true; + } + select_matching_brackets = true; + } + + // Attempt to select a sentence, for instance: int a = 2; + if(!is_bracket_language) { // If for instance cmake, meson or python + if(!select_matching_brackets) { + bool select_end_block = language->get_id() == "cmake" || language->get_id() == "meson"; + + auto get_tabs = [this](Gtk::TextIter iter) { + iter = get_buffer()->get_iter_at_line(iter.get_line()); + int tabs = 0; + while(!iter.ends_line() && (*iter == ' ' || *iter == '\t')) { + tabs++; + if(!iter.forward_char()) + break; + } + if(iter.ends_line()) + return -1; + return tabs; + }; + + // Forward to code iter + forward_to_code(start_stored); + if(start_stored > end_stored) + end_stored = start_stored; + + // Forward start to non-empty line + start = start_stored; + start = get_buffer()->get_iter_at_line(start.get_line()); + while(!start.is_end() && (*start == ' ' || *start == '\t') && start.forward_char()) { + } + + // Forward end to end of line + end = end_stored; + if(!end.ends_line()) + end.forward_to_line_end(); + + // Try select block that starts at cursor + auto end_tabs = get_tabs(end); + auto iter = end; + if(end_tabs >= 0) { + bool can_select_end_block = false; + while(iter.forward_char()) { + auto tabs = get_tabs(iter); + if(tabs < 0 || tabs > end_tabs || (select_end_block && can_select_end_block && tabs == end_tabs)) { + if(!iter.ends_line()) + iter.forward_to_line_end(); + end = iter; + if(tabs > end_tabs) + can_select_end_block = true; + if(tabs == end_tabs) + break; + continue; + } + break; + } + } + while(end > end_stored && end.starts_line() && end.ends_line() && end.backward_char()) { + } + + if(start == start_stored && end == end_stored) { // Try select block that cursor is within + // Backward start to line with less indentation + auto iter = get_buffer()->get_iter_at_line(start.get_line()); + auto start_tabs = get_tabs(iter); + if(start_tabs >= 0) { + while(iter.backward_char()) { + auto tabs = get_tabs(iter); + iter = get_buffer()->get_iter_at_line(iter.get_line()); + if(tabs >= 0 && tabs < start_tabs) { + start = iter; + break; + } + } + } + // Forward start to non-empty line + start = get_buffer()->get_iter_at_line(start.get_line()); + while(!start.is_end() && (*start == ' ' || *start == '\t') && start.forward_char()) { + } + + if(start != start_stored) { + // Forward end through lines with higher indentation + start_tabs = get_tabs(start); + iter = end; + if(start_tabs >= 0) { + while(iter.forward_char()) { + auto tabs = get_tabs(iter); + if(tabs < 0 || tabs > start_tabs || (select_end_block && tabs == start_tabs)) { + if(!iter.ends_line()) + iter.forward_to_line_end(); + end = iter; + if(tabs == start_tabs) + break; + continue; + } + break; + } + } + while(end > end_stored && end.starts_line() && end.ends_line() && end.backward_char()) { + } + } + + if(start == start_stored && end == end_stored) { + start = get_buffer()->begin(); + end = get_buffer()->end(); + } + } + get_buffer()->select_range(start, end); + return; + } + } + else if(!comma_used && end_sentence_iter && end > end_sentence_iter) { + if(!start_sentence_iter) + start_sentence_iter = start; + else + start_sentence_iter.forward_char(); + + // Forward to code iter (move passed macros) + while(forward_to_code(start_sentence_iter) && *start_sentence_iter == '#' && start_sentence_iter.forward_to_line_end()) { + auto prev = start_sentence_iter; + if(prev.backward_char() && *prev == '\\' && start_sentence_iter.forward_char()) { + while(start_sentence_iter.forward_to_line_end()) { + prev = start_sentence_iter; + if(prev.backward_char() && *prev == '\\' && start_sentence_iter.forward_char()) + continue; + break; + } + } + } + + end_sentence_iter.forward_char(); + if((end_sentence_iter != end_stored || start_sentence_iter != start_stored) && + ((*start == '{' && *end == '}') || (start.is_start() && end.is_end()))) { + start = start_sentence_iter; + end = end_sentence_iter; + select_matching_brackets = false; + } + } + if(select_matching_brackets) + start.forward_char(); + + if(start == start_stored && end == end_stored) { // In case of no change due to inbalanced brackets + previous_extended_selections.pop_back(); + if(!start.backward_char() && !end.forward_char()) + return; + get_buffer()->select_range(start, end); + extend_selection(); + return; + } + + get_buffer()->select_range(start, end); + return; +} + +void Source::View::shrink_selection() { + if(previous_extended_selections.empty()) { + Info::get().print("No previous extended selections found"); + return; + } + auto selection = previous_extended_selections.back(); + keep_previous_extended_selections = true; + get_buffer()->select_range(selection.first, selection.second); + hide_tooltips(); + keep_previous_extended_selections = false; + previous_extended_selections.pop_back(); +} + void Source::View::show_or_hide() { Gtk::TextIter start, end; get_buffer()->get_selection_bounds(start, end); @@ -1065,7 +1604,7 @@ void Source::View::show_or_hide() { break; } static std::vector exact = {"}", ")", "]", ">", " followed_by_non_token_char = {"elseif", "elif", "case", "default", "private", "public", "protected"}; + static std::vector followed_by_non_token_char = {"elseif", "elif", "catch", "case", "default", "private", "public", "protected"}; if(text == "{") { // C/C++ sometimes starts a block with a standalone { if(!is_token_char(*last_tabs_end)) { end = get_buffer()->get_iter_at_line(end.get_line()); @@ -1183,17 +1722,25 @@ void Source::View::place_cursor_at_next_diagnostic() { } } -Gtk::TextIter Source::View::find_non_whitespace_code_iter_backward(Gtk::TextIter iter) { - if(iter.starts_line()) - return iter; +bool Source::View::backward_to_code(Gtk::TextIter &iter) { + while((*iter == ' ' || *iter == '\t' || *iter == '\n' || iter.ends_line() || !is_code_iter(iter)) && iter.backward_char()) { + } + return !iter.is_start() || is_code_iter(iter); +} + +bool Source::View::forward_to_code(Gtk::TextIter &iter) { + while((*iter == ' ' || *iter == '\t' || *iter == '\n' || iter.ends_line() || !is_code_iter(iter)) && iter.forward_char()) { + } + return !iter.is_end(); +} + +void Source::View::backward_to_code_or_line_start(Gtk::TextIter &iter) { while(!iter.starts_line() && (!is_code_iter(iter) || *iter == ' ' || *iter == '\t' || iter.ends_line()) && iter.backward_char()) { } - return iter; } Gtk::TextIter Source::View::get_start_of_expression(Gtk::TextIter iter) { - while(!iter.starts_line() && (*iter == ' ' || *iter == '\t' || iter.ends_line() || !is_code_iter(iter)) && iter.backward_char()) { - } + backward_to_code_or_line_start(iter); if(iter.starts_line()) return iter; @@ -1259,15 +1806,13 @@ Gtk::TextIter Source::View::get_start_of_expression(Gtk::TextIter iter) { // Handle ',', ':', or operators that can be used between two lines, on previous line: auto previous_iter = iter; previous_iter.backward_char(); - while(!previous_iter.starts_line() && (*previous_iter == ' ' || previous_iter.ends_line() || !is_code_iter(previous_iter)) && previous_iter.backward_char()) { - } + backward_to_code_or_line_start(previous_iter); if(previous_iter.starts_line()) return iter; // Handle for instance: Test::Test():\n test(2) { if(has_open_curly && *previous_iter == ':') { previous_iter.backward_char(); - while(!previous_iter.starts_line() && *previous_iter == ' ' && previous_iter.backward_char()) { - } + backward_to_code_or_line_start(previous_iter); if(*previous_iter == ')') { auto token = get_token(get_tabs_end_iter(previous_iter)); if(token != "case") @@ -1290,36 +1835,19 @@ Gtk::TextIter Source::View::get_start_of_expression(Gtk::TextIter iter) { return iter; } -bool Source::View::find_open_curly_bracket_backward(Gtk::TextIter iter, Gtk::TextIter &found_iter) { - long count = 0; - - do { - if(*iter == '{') { - if(count == 0 && is_code_iter(iter)) { - found_iter = iter; - return true; - } - count++; - } - else if(*iter == '}' && is_code_iter(iter)) - count--; - } while(iter.backward_char()); - return false; -} - bool Source::View::find_close_symbol_forward(Gtk::TextIter iter, Gtk::TextIter &found_iter, unsigned int positive_char, unsigned int negative_char) { long count = 0; if(positive_char == '{' && negative_char == '}') { do { - if(*iter == negative_char && is_code_iter(iter)) { + if(*iter == positive_char && is_code_iter(iter)) + count++; + else if(*iter == negative_char && is_code_iter(iter)) { if(count == 0) { found_iter = iter; return true; } count--; } - else if(*iter == positive_char && is_code_iter(iter)) - count++; } while(iter.forward_char()); return false; } @@ -1347,6 +1875,46 @@ bool Source::View::find_close_symbol_forward(Gtk::TextIter iter, Gtk::TextIter & } } +bool Source::View::find_open_symbol_backward(Gtk::TextIter iter, Gtk::TextIter &found_iter, unsigned int positive_char, unsigned int negative_char) { + long count = 0; + if(positive_char == '{' && negative_char == '}') { + do { + if(*iter == positive_char && is_code_iter(iter)) { + if(count == 0) { + found_iter = iter; + return true; + } + count++; + } + else if(*iter == negative_char && is_code_iter(iter)) + count--; + } while(iter.backward_char()); + return false; + } + else { + long curly_count = 0; + do { + if(*iter == positive_char && is_code_iter(iter)) { + if(count == 0) { + found_iter = iter; + return true; + } + count++; + } + else if(*iter == negative_char && is_code_iter(iter)) + count--; + else if(*iter == '{' && is_code_iter(iter)) { + if(curly_count == 0) + return false; + curly_count++; + } + else if(*iter == '}' && is_code_iter(iter)) + curly_count--; + } while(iter.backward_char()); + return false; + } +} + long Source::View::symbol_count(Gtk::TextIter iter, unsigned int positive_char, unsigned int negative_char) { auto iter_stored = iter; long symbol_count = 0; @@ -1492,9 +2060,7 @@ bool Source::View::is_possible_argument() { auto iter = get_buffer()->get_insert()->get_iter(); if(iter.backward_char() && (!interactive_completion || last_keyval == '(' || last_keyval == ',' || last_keyval == ' ' || last_keyval == GDK_KEY_Return || last_keyval == GDK_KEY_KP_Enter)) { - while((*iter == ' ' || *iter == '\t' || *iter == '\n' || *iter == '\r') && iter.backward_char()) { - } - if(*iter == '(' || *iter == ',') + if(backward_to_code(iter) && (*iter == '(' || *iter == ',')) return true; } return false; @@ -1637,7 +2203,7 @@ bool Source::View::on_key_press_event_basic(GdkEventKey *key) { iter = get_buffer()->get_insert()->get_iter(); auto condition_iter = iter; condition_iter.backward_char(); - condition_iter = find_non_whitespace_code_iter_backward(condition_iter); + backward_to_code_or_line_start(condition_iter); auto start_iter = get_start_of_expression(condition_iter); auto tabs_end_iter = get_tabs_end_iter(start_iter); auto tabs = get_line_before(tabs_end_iter); @@ -1782,8 +2348,6 @@ bool Source::View::on_key_press_event_basic(GdkEventKey *key) { break; } if(iter.ends_line()) { - if(*iter == '\r') // For CR+LF - iter.forward_char(); if(!iter.forward_char()) do_smart_delete = false; break; @@ -1901,7 +2465,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { } auto condition_iter = previous_iter; - condition_iter = find_non_whitespace_code_iter_backward(condition_iter); + backward_to_code_or_line_start(condition_iter); auto start_iter = get_start_of_expression(condition_iter); auto tabs_end_iter = get_tabs_end_iter(start_iter); auto tabs = get_line_before(tabs_end_iter); @@ -2014,7 +2578,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { if(is_cpp && tabs_end_iter.starts_line()) { auto iter = condition_iter; Gtk::TextIter open_iter; - if(iter.backward_char() && find_open_curly_bracket_backward(iter, open_iter)) { + if(iter.backward_char() && find_open_symbol_backward(iter, open_iter, '{', '}')) { if(open_iter.starts_line()) // in case of: namespace test\n{ open_iter.backward_char(); auto iter = get_buffer()->get_iter_at_line(open_iter.get_line()); @@ -2145,7 +2709,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { auto previous_end_iter = start_iter; while(previous_end_iter.backward_char() && !previous_end_iter.ends_line()) { } - previous_end_iter = find_non_whitespace_code_iter_backward(previous_end_iter); + backward_to_code_or_line_start(previous_end_iter); auto previous_start_iter = get_tabs_end_iter(get_buffer()->get_iter_at_line(get_start_of_expression(previous_end_iter).get_line())); auto previous_tabs = get_line_before(previous_start_iter); if(!previous_end_iter.ends_line()) @@ -2164,8 +2728,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { auto iter = condition_iter; if(!iter.starts_line()) iter.backward_char(); - while(!iter.starts_line() && *iter == ' ' && iter.backward_char()) { - } + backward_to_code_or_line_start(iter); if(*iter == ')') { auto token = get_token(get_tabs_end_iter(get_buffer()->get_iter_at_line(iter.get_line()))); if(token != "case") // Do not move left for instance: void Test::Test(): @@ -2174,7 +2737,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { if(perform_indent) { Gtk::TextIter found_curly_iter; - if(find_open_curly_bracket_backward(iter, found_curly_iter)) { + if(find_open_symbol_backward(iter, found_curly_iter, '{', '}')) { auto tabs_end_iter = get_tabs_end_iter(get_buffer()->get_iter_at_line(found_curly_iter.get_line())); auto tabs_start_of_sentence = get_line_before(tabs_end_iter); if(tabs.size() == (tabs_start_of_sentence.size() + tab_size)) { @@ -2247,7 +2810,8 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { auto previous_end_iter = iter; while(previous_end_iter.backward_char() && !previous_end_iter.ends_line()) { } - auto condition_iter = find_non_whitespace_code_iter_backward(previous_end_iter); + auto condition_iter = previous_end_iter; + backward_to_code_or_line_start(condition_iter); auto previous_start_iter = get_tabs_end_iter(get_buffer()->get_iter_at_line(get_start_of_expression(condition_iter).get_line())); auto previous_tabs = get_line_before(previous_start_iter); auto after_condition_iter = condition_iter; @@ -2284,13 +2848,13 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { auto condition_iter = iter; while(condition_iter.starts_line() && condition_iter.backward_char()) { } - condition_iter = find_non_whitespace_code_iter_backward(condition_iter); + backward_to_code_or_line_start(condition_iter); if(*condition_iter == ';' && condition_iter.get_line() > 0 && is_code_iter(condition_iter)) { auto start_iter = get_start_of_expression(condition_iter); auto previous_end_iter = start_iter; while(previous_end_iter.backward_char() && !previous_end_iter.ends_line()) { } - previous_end_iter = find_non_whitespace_code_iter_backward(previous_end_iter); + backward_to_code_or_line_start(previous_end_iter); auto previous_start_iter = get_tabs_end_iter(get_buffer()->get_iter_at_line(get_start_of_expression(previous_end_iter).get_line())); auto previous_tabs = get_line_before(previous_start_iter); if(!previous_end_iter.ends_line()) @@ -2534,7 +3098,7 @@ bool Source::View::on_key_press_event_smart_inserts(GdkEventKey *key) { // Special case for functions and classes with no indentation after: namespace {: if(is_cpp && tabs_end_iter.starts_line()) { Gtk::TextIter open_iter; - if(find_open_curly_bracket_backward(iter, open_iter)) { + if(find_open_symbol_backward(iter, open_iter, '{', '}')) { if(open_iter.starts_line()) // in case of: namespace test\n{ open_iter.backward_char(); auto iter = get_buffer()->get_iter_at_line(open_iter.get_line()); diff --git a/src/source.h b/src/source.h index 87876d2..0fdeede 100644 --- a/src/source.h +++ b/src/source.h @@ -84,6 +84,9 @@ namespace Source { void hide_tooltips() override; void hide_dialogs() override; + void extend_selection(); + void shrink_selection(); + void show_or_hide(); /// Show or hide text selection bool soft_reparse_needed = false; @@ -120,13 +123,15 @@ namespace Source { gdouble on_motion_last_x = 0.0; gdouble on_motion_last_y = 0.0; - Gtk::TextIter find_non_whitespace_code_iter_backward(Gtk::TextIter iter); + bool backward_to_code(Gtk::TextIter &iter); + bool forward_to_code(Gtk::TextIter &iter); + void backward_to_code_or_line_start(Gtk::TextIter &iter); /// If closing bracket is found, continues until the open bracket. /// Returns if open bracket is found that has no corresponding closing bracket. /// Else, return at start of line. Gtk::TextIter get_start_of_expression(Gtk::TextIter iter); - bool find_open_curly_bracket_backward(Gtk::TextIter iter, Gtk::TextIter &found_iter); bool find_close_symbol_forward(Gtk::TextIter iter, Gtk::TextIter &found_iter, unsigned int positive_char, unsigned int negative_char); + bool find_open_symbol_backward(Gtk::TextIter iter, Gtk::TextIter &found_iter, unsigned int positive_char, unsigned int negative_char); long symbol_count(Gtk::TextIter iter, unsigned int positive_char, unsigned int negative_char); bool is_templated_function(Gtk::TextIter iter, Gtk::TextIter &parenthesis_end_iter); /// If insert is at an possible argument. Also based on last key press. @@ -154,6 +159,9 @@ namespace Source { bool is_cpp = false; guint previous_non_modifier_keyval = 0; + bool keep_previous_extended_selections = false; + std::vector> previous_extended_selections; + bool on_key_press_event_extra_cursors(GdkEventKey *key); }; } // namespace Source diff --git a/src/source_base.cc b/src/source_base.cc index e390bce..c36bf89 100644 --- a/src/source_base.cc +++ b/src/source_base.cc @@ -607,7 +607,7 @@ Gtk::TextIter Source::BaseView::get_tabs_end_iter() { } bool Source::BaseView::is_token_char(gunichar chr) { - if((chr >= 'A' && chr <= 'Z') || (chr >= 'a' && chr <= 'z') || (chr >= '0' && chr <= '9') || chr == '_') + if((chr >= 'A' && chr <= 'Z') || (chr >= 'a' && chr <= 'z') || (chr >= '0' && chr <= '9') || chr == '_' || chr >= 128) return true; return false; } diff --git a/src/source_generic.cc b/src/source_generic.cc index 7d07161..6b1e832 100644 --- a/src/source_generic.cc +++ b/src/source_generic.cc @@ -79,36 +79,13 @@ void Source::GenericView::parse_language_file(bool &has_context_class, const boo } } -bool Source::GenericView::is_word_iter(const Gtk::TextIter &iter) { - if(((*iter >= '0' && *iter <= '9') || (*iter >= 'A' && *iter <= 'Z') || (*iter >= 'a' && *iter <= 'z') || *iter >= 128)) - return true; - return false; -} - -std::pair Source::GenericView::get_word(Gtk::TextIter iter) { - auto start = iter; - auto end = iter; - - while(is_word_iter(iter)) { - start = iter; - if(!iter.backward_char()) - break; - } - while(is_word_iter(end)) { - if(!end.forward_char()) - break; - } - - return {start, end}; -} - std::vector> Source::GenericView::get_words(const Gtk::TextIter &start, const Gtk::TextIter &end) { std::vector> words; auto iter = start; while(iter && iter < end) { - if(is_word_iter(iter)) { - auto word = get_word(iter); + if(is_token_char(*iter)) { + auto word = get_token_iters(iter); if(!(*word.first >= '0' && *word.first <= '9') && (word.second.get_offset() - word.first.get_offset()) >= 3) // Minimum word length: 3 words.emplace_back(word.first, word.second); iter = word.second; @@ -133,11 +110,11 @@ void Source::GenericView::setup_buffer_words() { // Remove changed word at insert get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &iter_, const Glib::ustring &text, int bytes) { auto iter = iter_; - if(!is_word_iter(iter)) + if(!is_token_char(*iter)) iter.backward_char(); - if(is_word_iter(iter)) { - auto word = get_word(iter); + if(is_token_char(*iter)) { + auto word = get_token_iters(iter); if(word.second.get_offset() - word.first.get_offset() >= 3) { LockGuard lock(buffer_words_mutex); auto it = buffer_words.find(get_buffer()->get_text(word.first, word.second)); @@ -156,7 +133,7 @@ void Source::GenericView::setup_buffer_words() { auto start = iter; auto end = iter; start.backward_chars(text.size()); - if(!is_word_iter(start)) + if(!is_token_char(*start)) start.backward_char(); end.forward_char(); @@ -173,7 +150,7 @@ void Source::GenericView::setup_buffer_words() { get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &start_, const Gtk::TextBuffer::iterator &end_) { auto start = start_; auto end = end_; - if(!is_word_iter(start)) + if(!is_token_char(*start)) start.backward_char(); end.forward_char(); auto words = get_words(start, end); @@ -192,10 +169,10 @@ void Source::GenericView::setup_buffer_words() { // Add new word resulting from erased text get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &start_, const Gtk::TextBuffer::iterator & /*end*/) { auto start = start_; - if(!is_word_iter(start)) + if(!is_token_char(*start)) start.backward_char(); - if(is_word_iter(start)) { - auto word = get_word(start); + if(is_token_char(*start)) { + auto word = get_token_iters(start); if(word.second.get_offset() - word.first.get_offset() >= 3) { LockGuard lock(buffer_words_mutex); auto result = buffer_words.emplace(get_buffer()->get_text(word.first, word.second), 1); diff --git a/src/source_generic.h b/src/source_generic.h index 3df7209..6a0c141 100644 --- a/src/source_generic.h +++ b/src/source_generic.h @@ -16,8 +16,6 @@ namespace Source { std::set keywords; - bool is_word_iter(const Gtk::TextIter &iter); - std::pair get_word(Gtk::TextIter iter); std::vector> get_words(const Gtk::TextIter &start, const Gtk::TextIter &end); Mutex buffer_words_mutex ACQUIRED_AFTER(autocomplete.prefix_mutex); diff --git a/src/window.cc b/src/window.cc index ba19b15..e868179 100644 --- a/src/window.cc +++ b/src/window.cc @@ -551,6 +551,16 @@ void Window::set_menu_actions() { } }); + menu.add_action("edit_extend_selection", []() { + if(auto view = Notebook::get().get_current_view()) + view->extend_selection(); + }); + + menu.add_action("edit_shrink_selection", []() { + if(auto view = Notebook::get().get_current_view()) + view->shrink_selection(); + }); + menu.add_action("edit_show_or_hide", []() { if(auto view = Notebook::get().get_current_view()) view->show_or_hide(); diff --git a/tests/source_test.cc b/tests/source_test.cc index b5a9f85..54216d0 100644 --- a/tests/source_test.cc +++ b/tests/source_test.cc @@ -174,4 +174,190 @@ int main() { assert(buffer->get_insert()->get_iter().get_line_offset() == 0); } } + + // extend_selection() tests + { + auto buffer = source_view.get_buffer(); + source_view.is_bracket_language = true; + std::string source = "test(1, test(10), \"100\");"; + buffer->set_text(source); + { + source_view.place_cursor_at_line_offset(0, 0); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test(1, test(10), \"100\")"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + { + source_view.place_cursor_at_line_offset(0, 5); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "1"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "1, test(10), \"100\""); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test(1, test(10), \"100\")"); + } + { + source_view.place_cursor_at_line_offset(0, 7); + source_view.extend_selection(); + assert(source_view.get_selected_text() == " test(10)"); + } + { + source_view.place_cursor_at_line_offset(0, 8); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test(10)"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == " test(10)"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "1, test(10), \"100\""); + } + { + source_view.place_cursor_at_line_offset(0, 18); + source_view.extend_selection(); + assert(source_view.get_selected_text() == " \"100\""); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "1, test(10), \"100\""); + } + { + source_view.place_cursor_at_line_offset(0, 26); + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + { + source_view.place_cursor_at_line_offset(0, 27); + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + + source = "int main() {\n return 1;\n}\n"; + buffer->set_text(source); + { + source_view.place_cursor_at_line_offset(0, 0); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "int"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + { + source_view.place_cursor_at_line_offset(0, 4); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "main"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(4, source.size() - 1 - 4)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + { + source_view.place_cursor_at_line_offset(1, 2); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "return"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "return 1;"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(12, 13)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(4, source.size() - 1 - 4)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + + source = "test(11, 22);"; + buffer->set_text(source); + { + source_view.place_cursor_at_line_offset(0, 0); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + } + { + source_view.place_cursor_at_line_offset(0, 5); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "int"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(5, 8)); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + } + { + source_view.place_cursor_at_line_offset(0, 15); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "11"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "11, 22"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source.substr(0, source.size() - 1)); + } + + source = "{\n {\n test;\n }\n}\n"; + buffer->set_text(source); + { + source_view.place_cursor_at_line_offset(2, 4); + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "test;"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "\n test;\n "); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "{\n test;\n }"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "\n {\n test;\n }\n"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == "{\n {\n test;\n }\n}"); + + source_view.extend_selection(); + assert(source_view.get_selected_text() == source); + + source_view.shrink_selection(); + assert(source_view.get_selected_text() == "{\n {\n test;\n }\n}"); + + source_view.shrink_selection(); + assert(source_view.get_selected_text() == "\n {\n test;\n }\n"); + + source_view.shrink_selection(); + assert(source_view.get_selected_text() == "{\n test;\n }"); + } + } }