diff --git a/CMakeLists.txt b/CMakeLists.txt index 5eb23eb..dd4201f 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.1") +set(JUCI_VERSION "1.4.6.2") set(CPACK_PACKAGE_NAME "jucipp") set(CPACK_PACKAGE_CONTACT "Ole Christian Eidheim ") diff --git a/README.md b/README.md index e757c4a..9dced28 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ towards libclang with speed, stability, and ease of use in mind. * Regex search and replace * Smart paste, keys and indentation * 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 * Source minimap * Split view diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 059bfd7..f2c9a1b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,10 +11,12 @@ set(JUCI_SHARED_FILES menu.cc meson.cc project_build.cc + snippets.cc source.cc source_base.cc source_clang.cc source_diff.cc + source_generic.cc source_language_protocol.cc source_spellcheck.cc terminal.cc diff --git a/src/autocomplete.h b/src/autocomplete.h index df224db..a1d54cd 100644 --- a/src/autocomplete.h +++ b/src/autocomplete.h @@ -16,7 +16,7 @@ class Autocomplete { public: enum class State { IDLE, STARTING, RESTARTING, CANCELED }; - std::string prefix; + Glib::ustring prefix; std::mutex prefix_mutex; std::vector rows; Tooltips tooltips; diff --git a/src/files.h b/src/files.h index b87c47c..9a66d44 100644 --- a/src/files.h +++ b/src/files.h @@ -72,6 +72,7 @@ const std::string default_config_file = R"RAW({ }, "keybindings": { "preferences": "comma", + "snippets": "", "quit": "q", "file_new_file": "n", "file_new_folder": "n", diff --git a/src/menu.cc b/src/menu.cc index 7c2d927..b5c19de 100644 --- a/src/menu.cc +++ b/src/menu.cc @@ -99,6 +99,12 @@ const Glib::ustring menu_xml = R"RAW( app.preferences +
+ + _Snippets + app.snippets + +
_Quit diff --git a/src/notebook.cc b/src/notebook.cc index 51f40d7..a89f001 100644 --- a/src/notebook.cc +++ b/src/notebook.cc @@ -6,6 +6,7 @@ #include "project.h" #include "selection_dialog.h" #include "source_clang.h" +#include "source_generic.h" #include "source_language_protocol.h" #include #include diff --git a/src/project.cc b/src/project.cc index f79bd80..2e40e61 100644 --- a/src/project.cc +++ b/src/project.cc @@ -12,6 +12,7 @@ #endif #include "ctags.h" #include "info.h" +#include "snippets.h" #include "source_clang.h" #include "source_language_protocol.h" #include "usages_clang.h" @@ -46,6 +47,13 @@ void Project::on_save(size_t index) { auto view = Notebook::get().get_view(index); if(!view) return; + + if(view->file_path == Config::get().home_juci_path / "snippets.json") { + Snippets::get().load(); + for(auto view : Notebook::get().get_views()) + view->set_snippets(); + } + boost::filesystem::path build_path; if(view->language && view->language->get_id() == "cmake") { if(view->file_path.filename() == "CMakeLists.txt") diff --git a/src/selection_dialog.cc b/src/selection_dialog.cc index f7d9f0b..a94cd06 100644 --- a/src/selection_dialog.cc +++ b/src/selection_dialog.cc @@ -403,10 +403,7 @@ bool CompletionDialog::on_key_release(GdkEventKey *key) { } bool CompletionDialog::on_key_press(GdkEventKey *key) { - if((key->keyval >= GDK_KEY_0 && key->keyval <= GDK_KEY_9) || - (key->keyval >= GDK_KEY_A && key->keyval <= GDK_KEY_Z) || - (key->keyval >= GDK_KEY_a && key->keyval <= GDK_KEY_z) || - key->keyval == GDK_KEY_underscore || key->keyval == GDK_KEY_BackSpace) { + if((key->keyval >= '0' && key->keyval <= '9') || (key->keyval >= 'a' && key->keyval <= 'z') || (key->keyval >= 'A' && key->keyval <= 'Z') || key->keyval == '_' || gdk_keyval_to_unicode(key->keyval) >= 0x00C0 || key->keyval == GDK_KEY_BackSpace) { if(row_in_entry) { text_view->get_buffer()->erase(start_mark->get_iter(), text_view->get_buffer()->get_insert()->get_iter()); row_in_entry = false; diff --git a/src/snippets.cc b/src/snippets.cc new file mode 100644 index 0000000..fafc6f3 --- /dev/null +++ b/src/snippets.cc @@ -0,0 +1,40 @@ +#include "snippets.h" +#include "config.h" +#include "filesystem.h" +#include "terminal.h" +#include + +Snippets::Snippets() { + load(); +} + +void Snippets::load() { + auto snippets_file = Config::get().home_juci_path / "snippets.json"; + + boost::system::error_code ec; + if(!boost::filesystem::exists(snippets_file, ec)) + filesystem::write(snippets_file, R"({ + "^markdown$": [ + { + "prefix": "code_block", + "body": "```${1:language}\n${2:code}\n```\n", + "description": "Insert code block" + } + ] + } + )"); + + snippets.clear(); + try { + boost::property_tree::ptree pt; + boost::property_tree::json_parser::read_json(snippets_file.string(), pt); + for(auto language_it = pt.begin(); language_it != pt.end(); ++language_it) { + snippets.emplace_back(std::regex(language_it->first), std::vector()); + for(auto snippet_it = language_it->second.begin(); snippet_it != language_it->second.end(); ++snippet_it) + snippets.back().second.emplace_back(Snippet{snippet_it->second.get("prefix"), snippet_it->second.get("body"), snippet_it->second.get("description", "")}); + } + } + catch(const std::exception &e) { + Terminal::get().async_print(std::string("Error: ") + e.what() + "\n", true); + } +} \ No newline at end of file diff --git a/src/snippets.h b/src/snippets.h new file mode 100644 index 0000000..456dc72 --- /dev/null +++ b/src/snippets.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class Snippets { + Snippets(); + +public: + class Snippet { + public: + std::string prefix; + std::string body; + std::string description; + }; + + static Snippets &get() { + static Snippets singleton; + return singleton; + } + + std::vector>> snippets; + void load(); +}; \ No newline at end of file diff --git a/src/source.cc b/src/source.cc index 76c5d10..86b7a4f 100644 --- a/src/source.cc +++ b/src/source.cc @@ -1350,7 +1350,11 @@ bool Source::View::on_key_press_event(GdkEventKey *key) { previous_non_modifier_keyval = last_keyval; last_keyval = key->keyval; - if(Config::get().source.enable_multiple_cursors && on_key_press_event_multiple_cursors(key)) + if((key->keyval == GDK_KEY_Tab || key->keyval == GDK_KEY_ISO_Left_Tab) && (key->state & GDK_SHIFT_MASK) == 0 && select_snippet_argument()) + return true; + else if(key->keyval == GDK_KEY_Escape && clear_snippet_marks()) + return true; + else if(Config::get().source.enable_multiple_cursors && on_key_press_event_extra_cursors(key)) return true; //Move cursor one paragraph down @@ -1740,7 +1744,7 @@ bool Source::View::on_key_press_event_bracket_language(GdkEventKey *key) { auto end_of_line_iter = start_iter; end_of_line_iter.forward_to_line_end(); auto line = get_buffer()->get_text(tabs_end_iter, end_of_line_iter); - if(!line.empty() && line.compare(0, 2, "*/") == 0) { + if(!line.empty() && line.raw().compare(0, 2, "*/") == 0) { tabs.pop_back(); get_buffer()->insert_at_cursor('\n' + tabs); scroll_to(get_buffer()->get_insert()); @@ -2185,9 +2189,9 @@ bool Source::View::on_key_press_event_smart_brackets(GdkEventKey *key) { } bool Source::View::on_key_press_event_smart_inserts(GdkEventKey *key) { - keep_argument_marks = true; + keep_snippet_marks = true; ScopeGuard guard{[this] { - keep_argument_marks = false; + keep_snippet_marks = false; }}; if(get_buffer()->get_has_selection()) { @@ -2453,80 +2457,15 @@ 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; - } - } +bool Source::View::on_key_press_event_extra_cursors(GdkEventKey *key) { + setup_extra_cursor_signals(); - if(mark->get_name() == "insert") { - if(enable_multiple_cursors) { - enable_multiple_cursors = 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); - } - } - enable_multiple_cursors = 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(enable_multiple_cursors) { - enable_multiple_cursors = 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); - } - enable_multiple_cursors = true; - } - }); - - get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &iter_start, const Gtk::TextBuffer::iterator &iter_end) { - if(enable_multiple_cursors) { - 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(enable_multiple_cursors) { - enable_multiple_cursors = 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); - } - enable_multiple_cursors = true; - } - }); - } - - - if(key->keyval == GDK_KEY_Escape && !multiple_cursors_extra_cursors.empty()) { - for(auto &extra_cursor : multiple_cursors_extra_cursors) { + if(key->keyval == GDK_KEY_Escape && !extra_cursors.empty()) { + for(auto &extra_cursor : extra_cursors) { extra_cursor.first->set_visible(false); get_buffer()->delete_mark(extra_cursor.first); } - multiple_cursors_extra_cursors.clear(); + extra_cursors.clear(); return true; } @@ -2535,27 +2474,27 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { // 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()) + if(extra_cursors.empty()) return false; - auto &cursor = multiple_cursors_extra_cursors.back().first; + auto &cursor = 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()) + if(extra_cursors.empty()) return false; - auto &cursor = multiple_cursors_extra_cursors.back().first; + auto &cursor = 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()) + if(extra_cursors.empty()) return false; - auto &extra_cursor = multiple_cursors_extra_cursors.back(); + auto &extra_cursor = extra_cursors.back(); auto iter = extra_cursor.first->get_iter(); auto line_offset = extra_cursor.second; if(iter.backward_line()) { @@ -2568,9 +2507,9 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { 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()) + if(extra_cursors.empty()) return false; - auto &extra_cursor = multiple_cursors_extra_cursors.back(); + auto &extra_cursor = extra_cursors.back(); auto iter = extra_cursor.first->get_iter(); auto line_offset = extra_cursor.second; if(iter.forward_line()) { @@ -2588,7 +2527,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { 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) + for(auto &extra_cursor : 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()) { @@ -2596,8 +2535,8 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { 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); + extra_cursors.emplace_back(get_buffer()->create_mark(iter, false), insert_line_offset); + extra_cursors.back().first->set_visible(true); } return true; } @@ -2605,7 +2544,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { 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) + for(auto &extra_cursor : 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()) { @@ -2613,8 +2552,8 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { 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); + extra_cursors.emplace_back(get_buffer()->create_mark(iter, false), insert_line_offset); + extra_cursors.back().first->set_visible(true); } return true; } @@ -2622,7 +2561,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { // Move cursors left/right if((key->keyval == GDK_KEY_Left || key->keyval == GDK_KEY_KP_Left) && (key->state & GDK_CONTROL_MASK) > 0) { enable_multiple_cursors = false; - for(auto &extra_cursor : multiple_cursors_extra_cursors) { + for(auto &extra_cursor : extra_cursors) { auto iter = extra_cursor.first->get_iter(); iter.backward_word_start(); extra_cursor.second = iter.get_line_offset(); @@ -2638,7 +2577,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { } if((key->keyval == GDK_KEY_Right || key->keyval == GDK_KEY_KP_Right) && (key->state & GDK_CONTROL_MASK) > 0) { enable_multiple_cursors = false; - for(auto &extra_cursor : multiple_cursors_extra_cursors) { + for(auto &extra_cursor : extra_cursors) { auto iter = extra_cursor.first->get_iter(); iter.forward_visible_word_end(); extra_cursor.second = iter.get_line_offset(); @@ -2656,7 +2595,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { // Move cursors up/down if((key->keyval == GDK_KEY_Up || key->keyval == GDK_KEY_KP_Up)) { enable_multiple_cursors = false; - for(auto &extra_cursor : multiple_cursors_extra_cursors) { + for(auto &extra_cursor : extra_cursors) { auto iter = extra_cursor.first->get_iter(); auto line_offset = extra_cursor.second; if(iter.backward_line()) { @@ -2671,7 +2610,7 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { } if((key->keyval == GDK_KEY_Down || key->keyval == GDK_KEY_KP_Down)) { enable_multiple_cursors = false; - for(auto &extra_cursor : multiple_cursors_extra_cursors) { + for(auto &extra_cursor : extra_cursors) { auto iter = extra_cursor.first->get_iter(); auto line_offset = extra_cursor.second; if(iter.forward_line()) { @@ -2687,14 +2626,14 @@ bool Source::View::on_key_press_event_multiple_cursors(GdkEventKey *key) { // 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) + for(auto &extra_cursor : extra_cursors) get_buffer()->move_mark(extra_cursor.first, get_smart_home_iter(extra_cursor.first->get_iter())); enable_multiple_cursors = 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) + for(auto &extra_cursor : extra_cursors) get_buffer()->move_mark(extra_cursor.first, get_smart_end_iter(extra_cursor.first->get_iter())); enable_multiple_cursors = false; return false; @@ -2775,82 +2714,3 @@ bool Source::View::on_motion_notify_event(GdkEventMotion *event) { return Gsv::View::on_motion_notify_event(event); #endif } - - -///////////////////// -//// GenericView //// -///////////////////// -Source::GenericView::GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), View(file_path, language, true) { - configure(); - spellcheck_all = true; - - auto completion = get_completion(); - completion->property_show_headers() = false; - completion->property_show_icons() = false; - completion->property_accelerators() = 0; - - auto completion_words = Gsv::CompletionWords::create("", Glib::RefPtr()); - completion_words->register_provider(get_buffer()); - completion->add_provider(completion_words); - - if(language) { - auto language_manager = LanguageManager::get_default(); - auto search_paths = language_manager->get_search_path(); - bool found_language_file = false; - boost::filesystem::path language_file; - for(auto &search_path : search_paths) { - boost::filesystem::path p(static_cast(search_path) + '/' + static_cast(language->get_id()) + ".lang"); - if(boost::filesystem::exists(p) && boost::filesystem::is_regular_file(p)) { - language_file = p; - found_language_file = true; - break; - } - } - if(found_language_file) { - auto completion_buffer_keywords = CompletionBuffer::create(); - boost::property_tree::ptree pt; - try { - boost::property_tree::xml_parser::read_xml(language_file.string(), pt); - } - catch(const std::exception &e) { - Terminal::get().print("Error: error parsing language file " + language_file.string() + ": " + e.what() + '\n', true); - } - bool has_context_class = false; - parse_language_file(completion_buffer_keywords, has_context_class, pt); - if(!has_context_class || language->get_id() == "cmake") // TODO: no longer use the spellcheck_all flag - spellcheck_all = false; - completion_words->register_provider(completion_buffer_keywords); - } - } -} - -void Source::GenericView::parse_language_file(Glib::RefPtr &completion_buffer, bool &has_context_class, const boost::property_tree::ptree &pt) { - bool case_insensitive = false; - for(auto &node : pt) { - if(node.first == "") { - auto data = static_cast(node.second.data()); - std::transform(data.begin(), data.end(), data.begin(), ::tolower); - if(data.find("case insensitive") != std::string::npos) - case_insensitive = true; - } - else if(node.first == "keyword") { - auto data = static_cast(node.second.data()); - completion_buffer->insert_at_cursor(data + '\n'); - if(case_insensitive) { - std::transform(data.begin(), data.end(), data.begin(), ::tolower); - completion_buffer->insert_at_cursor(data + '\n'); - } - } - else if(!has_context_class && node.first == "context") { - auto class_attribute = node.second.get(".class", ""); - auto class_disabled_attribute = node.second.get(".class-disabled", ""); - if(class_attribute.size() > 0 || class_disabled_attribute.size() > 0) - has_context_class = true; - } - try { - parse_language_file(completion_buffer, has_context_class, node.second); - } - catch(const std::exception &e) { - } - } -} diff --git a/src/source.h b/src/source.h index 508de04..3f8dc0a 100644 --- a/src/source.h +++ b/src/source.h @@ -133,8 +133,6 @@ namespace Source { bool on_button_press_event(GdkEventButton *event) override; bool on_motion_notify_event(GdkEventMotion *motion_event) override; - /// After autocomplete, arguments could be marked so that one can use tab to select the next argument - bool keep_argument_marks = false; bool interactive_completion = true; private: @@ -149,24 +147,6 @@ namespace Source { bool is_cpp = false; guint previous_non_modifier_keyval = 0; - bool multiple_cursors_signals_set = 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 { - private: - class CompletionBuffer : public Gtk::TextBuffer { - public: - static Glib::RefPtr create() { return Glib::RefPtr(new CompletionBuffer()); } - }; - - public: - GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language); - - void parse_language_file(Glib::RefPtr &completion_buffer, bool &has_context_class, const boost::property_tree::ptree &pt); + bool on_key_press_event_extra_cursors(GdkEventKey *key); }; } // namespace Source diff --git a/src/source_base.cc b/src/source_base.cc index 9484e0f..ba5091d 100644 --- a/src/source_base.cc +++ b/src/source_base.cc @@ -6,6 +6,7 @@ #include "utility.h" #include #include +#include Source::BaseView::BaseView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : Gsv::View(), file_path(file_path), language(language), status_diagnostics(0, 0, 0) { load(true); @@ -58,6 +59,15 @@ Source::BaseView::BaseView(const boost::filesystem::path &file_path, const Glib: search_context = gtk_source_search_context_new(get_source_buffer()->gobj(), search_settings); gtk_source_search_context_set_highlight(search_context, true); g_signal_connect(search_context, "notify::occurrences-count", G_CALLBACK(search_occurrences_updated), this); + + + set_snippets(); + + snippet_argument_tag = get_buffer()->create_tag(); + Gdk::RGBA rgba; + rgba.set_rgba(0.5, 0.5, 0.5, 0.4); + snippet_argument_tag->property_background_rgba() = rgba; + snippet_argument_tag->property_background_set() = true; } Source::BaseView::~BaseView() { @@ -801,7 +811,6 @@ void Source::BaseView::paste() { // add final newline if present in text if(text.size() > 0 && text.back() == '\n') get_buffer()->insert_at_cursor('\n' + prefix_tabs); - get_buffer()->place_cursor(get_buffer()->get_insert()->get_iter()); get_buffer()->end_user_action(); scroll_to_cursor_delayed(this, false, false); } @@ -916,3 +925,225 @@ void Source::BaseView::search_occurrences_updated(GtkWidget *widget, GParamSpec if(view->update_search_occurrences) view->update_search_occurrences(gtk_source_search_context_get_occurrences_count(view->search_context)); } + +void Source::BaseView::set_snippets() { + std::lock_guard lock(snippets_mutex); + + snippets = nullptr; + + if(language) { + for(auto &pair : Snippets::get().snippets) { + std::smatch sm; + if(std::regex_match(language->get_id().raw(), sm, pair.first)) { + snippets = &pair.second; + break; + } + } + } +} + +void Source::BaseView::setup_extra_cursor_signals() { + if(extra_cursors_signals_set) + return; + extra_cursors_signals_set = true; + + auto last_insert = get_buffer()->create_mark(get_buffer()->get_insert()->get_iter(), false); + get_buffer()->signal_mark_set().connect([this, last_insert](const Gtk::TextBuffer::iterator &iter, const Glib::RefPtr &mark) mutable { + for(auto &extra_cursor : extra_cursors) { + if(extra_cursor.first == mark && !iter.ends_line()) { + extra_cursor.second = std::max(extra_cursor.second, iter.get_line_offset()); + break; + } + } + + if(mark->get_name() == "insert") { + if(!keep_snippet_marks) + clear_snippet_marks(); + + if(enable_multiple_cursors) { + enable_multiple_cursors = false; + auto offset_diff = mark->get_iter().get_offset() - last_insert->get_iter().get_offset(); + if(offset_diff != 0) { + for(auto &extra_cursor : extra_cursors) { + auto iter = extra_cursor.first->get_iter(); + iter.forward_chars(offset_diff); + get_buffer()->move_mark(extra_cursor.first, iter); + } + for(auto &extra_cursor : extra_snippet_cursors) { + auto iter = extra_cursor->get_iter(); + iter.forward_chars(offset_diff); + get_buffer()->move_mark(extra_cursor, iter); + } + } + enable_multiple_cursors = true; + } + get_buffer()->move_mark(last_insert, mark->get_iter()); + } + }); + + get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &iter, const Glib::ustring &text, int bytes) { + if(enable_multiple_cursors && (!extra_cursors.empty() || !extra_snippet_cursors.empty())) { + enable_multiple_cursors = false; + auto offset = iter.get_offset() - get_buffer()->get_insert()->get_iter().get_offset(); + if(offset > 0) + offset -= text.size(); + for(auto &extra_cursor : extra_cursors) { + auto iter = extra_cursor.first->get_iter(); + iter.forward_chars(offset); + get_buffer()->insert(iter, text); + auto extra_cursor_iter = extra_cursor.first->get_iter(); + if(!extra_cursor_iter.ends_line()) + extra_cursor.second = extra_cursor_iter.get_line_offset(); + } + for(auto &extra_cursor : extra_snippet_cursors) { + auto iter = extra_cursor->get_iter(); + iter.forward_chars(offset); + get_buffer()->insert(iter, text); + } + enable_multiple_cursors = true; + } + }); + + auto erase_backward_length = std::make_shared(0); + auto erase_forward_length = std::make_shared(0); + get_buffer()->signal_erase().connect([this, erase_backward_length, erase_forward_length](const Gtk::TextBuffer::iterator &iter_start, const Gtk::TextBuffer::iterator &iter_end) { + if(enable_multiple_cursors && (!extra_cursors.empty() || !extra_snippet_cursors.empty())) { + auto insert_offset = get_buffer()->get_insert()->get_iter().get_offset(); + *erase_backward_length = insert_offset - iter_start.get_offset(); + *erase_forward_length = iter_end.get_offset() - insert_offset; + } + }, false); + get_buffer()->signal_erase().connect([this, erase_backward_length, erase_forward_length](const Gtk::TextBuffer::iterator &iter_start, const Gtk::TextBuffer::iterator &iter_end) { + if(enable_multiple_cursors && (*erase_backward_length != 0 || *erase_forward_length != 0)) { + enable_multiple_cursors = false; + for(auto &extra_cursor : extra_cursors) { + auto start_iter = extra_cursor.first->get_iter(); + auto end_iter = start_iter; + start_iter.backward_chars(*erase_backward_length); + end_iter.forward_chars(*erase_forward_length); + get_buffer()->erase(start_iter, end_iter); + auto extra_cursor_iter = extra_cursor.first->get_iter(); + if(!extra_cursor_iter.ends_line()) + extra_cursor.second = extra_cursor_iter.get_line_offset(); + } + for(auto &extra_cursor : extra_snippet_cursors) { + auto start_iter = extra_cursor->get_iter(); + auto end_iter = start_iter; + start_iter.backward_chars(*erase_backward_length); + end_iter.forward_chars(*erase_forward_length); + get_buffer()->erase(start_iter, end_iter); + } + enable_multiple_cursors = true; + *erase_backward_length = 0; + *erase_forward_length = 0; + } + }); +} + +void Source::BaseView::insert_snippet(Gtk::TextIter iter, Glib::ustring snippet) { + std::map>> arguments_offsets; + size_t pos1 = 0; + while((pos1 = snippet.find("${", pos1)) != Glib::ustring::npos) { + size_t pos2 = snippet.find(":", pos1 + 2); + if(pos2 != Glib::ustring::npos) { + size_t pos3 = snippet.find("}", pos2 + 1); + if(pos3 != Glib::ustring::npos) { + try { + auto number = std::stoul(snippet.substr(pos1 + 2, pos2 - pos1 - 2)); + size_t length = pos3 - pos2 - 1; + snippet.erase(pos3, 1); + snippet.erase(pos1, pos2 - pos1 + 1); + arguments_offsets[number].emplace_back(pos1, pos1 + length); + pos1 += length; + continue; + } + catch(...) { + } + } + } + break; + } + + auto mark = get_buffer()->create_mark(iter); + get_buffer()->insert(iter, snippet); + iter = mark->get_iter(); + get_buffer()->delete_mark(mark); + for(auto arguments_offsets_it = arguments_offsets.rbegin(); arguments_offsets_it != arguments_offsets.rend(); ++arguments_offsets_it) { + snippets_marks.emplace_front(); + for(auto &offsets : arguments_offsets_it->second) { + auto start = iter; + auto end = start; + start.forward_chars(offsets.first); + end.forward_chars(offsets.second); + snippets_marks.front().emplace_back(get_buffer()->create_mark(start), get_buffer()->create_mark(end)); + get_buffer()->apply_tag(snippet_argument_tag, start, end); + } + } + + if(!arguments_offsets.empty()) + select_snippet_argument(); +} + +bool Source::BaseView::select_snippet_argument() { + if(!extra_snippet_cursors.empty()) { + for(auto &extra_cursor : extra_snippet_cursors) { + extra_cursor->set_visible(false); + get_buffer()->delete_mark(extra_cursor); + } + extra_snippet_cursors.clear(); + } + + if(!snippets_marks.empty()) { + auto snippets_marks_it = snippets_marks.begin(); + bool first = true; + for(auto &marks : *snippets_marks_it) { + auto start = marks.first->get_iter(); + auto end = marks.second->get_iter(); + if(first) { + keep_snippet_marks = true; + get_buffer()->select_range(start, end); + keep_snippet_marks = false; + first = false; + } + else { + extra_snippet_cursors.emplace_back(get_buffer()->create_mark(start, false)); + extra_snippet_cursors.back()->set_visible(true); + + setup_extra_cursor_signals(); + } + get_buffer()->delete_mark(marks.first); + get_buffer()->delete_mark(marks.second); + } + snippets_marks.erase(snippets_marks_it); + return true; + } + return false; +} + +bool Source::BaseView::clear_snippet_marks() { + bool cleared = false; + + if(!snippets_marks.empty()) { + for(auto &snippet_marks : snippets_marks) { + for(auto &pair : snippet_marks) { + get_buffer()->delete_mark(pair.first); + get_buffer()->delete_mark(pair.second); + } + } + snippets_marks.clear(); + cleared = true; + } + + if(!extra_snippet_cursors.empty()) { + for(auto &extra_cursor : extra_snippet_cursors) { + extra_cursor->set_visible(false); + get_buffer()->delete_mark(extra_cursor); + } + extra_snippet_cursors.clear(); + cleared = true; + } + + get_buffer()->remove_tag(snippet_argument_tag, get_buffer()->begin(), get_buffer()->end()); + + return cleared; +} diff --git a/src/source_base.h b/src/source_base.h index f5abf00..2e5f87b 100644 --- a/src/source_base.h +++ b/src/source_base.h @@ -1,9 +1,13 @@ #pragma once +#include "snippets.h" #include #include +#include #include +#include #include +#include namespace Source { class BaseView : public Gsv::View { @@ -75,6 +79,8 @@ namespace Source { bool disable_spellcheck = false; + void set_snippets(); + private: GtkSourceSearchContext *search_context; GtkSourceSearchSettings *search_settings; @@ -121,5 +127,20 @@ namespace Source { void cleanup_whitespace_characters(const Gtk::TextIter &iter); bool enable_multiple_cursors = false; + + std::vector, int>> extra_cursors; + std::vector> extra_snippet_cursors; + void setup_extra_cursor_signals(); + bool extra_cursors_signals_set = false; + + /// After inserting a snippet, one can use tab to select the next argument + bool keep_snippet_marks = false; + std::vector *snippets = nullptr; + std::mutex snippets_mutex; + std::list, Glib::RefPtr>>> snippets_marks; + Glib::RefPtr snippet_argument_tag; + void insert_snippet(Gtk::TextIter iter, Glib::ustring snippet); + bool select_snippet_argument(); + bool clear_snippet_marks(); }; } // namespace Source diff --git a/src/source_clang.cc b/src/source_clang.cc index fef8b33..b65e4a6 100644 --- a/src/source_clang.cc +++ b/src/source_clang.cc @@ -540,6 +540,7 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa if(!is_code_iter(iter)) return false; + enable_snippets = false; show_parameters = false; auto line = ' ' + get_line_before(); @@ -551,6 +552,8 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa { std::unique_lock lock(autocomplete.prefix_mutex); autocomplete.prefix = sm.length(2) ? sm[3].str() : sm.length(4) ? sm[5].str() : sm[6].str(); + if(!sm.length(2) && !sm.length(4)) + enable_snippets = true; } return true; } @@ -567,8 +570,20 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa } if(iter != end_iter) iter.forward_char(); - std::unique_lock lock(autocomplete.prefix_mutex); - autocomplete.prefix = get_buffer()->get_text(iter, end_iter); + + { + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix = get_buffer()->get_text(iter, end_iter); + } + auto prev1 = iter; + if(prev1.backward_char() && *prev1 != '.') { + auto prev2 = prev1; + if(!prev2.backward_char()) + enable_snippets = true; + else if(!(*prev2 == ':' && *prev1 == ':') && !(*prev2 == '-' && *prev1 == '>')) + enable_snippets = true; + } + return true; } @@ -605,13 +620,15 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa } if(autocomplete.state == Autocomplete::State::STARTING) { - std::string prefix_copy; + std::string prefix; { std::lock_guard lock(autocomplete.prefix_mutex); - prefix_copy = autocomplete.prefix; + prefix = autocomplete.prefix; } completion_strings.clear(); + snippet_inserts.clear(); + snippet_comments.clear(); for(unsigned i = 0; i < code_complete_results->size(); ++i) { auto result = code_complete_results->get(i); if(result.available()) { @@ -654,7 +671,7 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa if(kind != clangmm::CompletionChunk_Informative) { auto chunk_cstr = clangmm::String(clang_getCompletionChunkText(result.cx_completion_string, i)); if(kind == clangmm::CompletionChunk_TypedText) { - if(strlen(chunk_cstr.c_str) >= prefix_copy.size() && prefix_copy.compare(0, prefix_copy.size(), chunk_cstr.c_str, prefix_copy.size()) == 0) + if(strlen(chunk_cstr.c_str) >= prefix.size() && prefix.compare(0, prefix.size(), chunk_cstr.c_str, prefix.size()) == 0) match = true; else break; @@ -674,6 +691,19 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa } } } + if(!show_parameters && enable_snippets) { + std::lock_guard lock(snippets_mutex); + if(snippets) { + for(auto &snippet : *snippets) { + if(prefix.compare(0, prefix.size(), snippet.prefix, 0, prefix.size()) == 0) { + autocomplete.rows.emplace_back(snippet.prefix); + completion_strings.emplace_back(nullptr); + snippet_inserts.emplace(autocomplete.rows.size() - 1, snippet.body); + snippet_comments.emplace(autocomplete.rows.size() - 1, snippet.description); + } + } + } + } } }; @@ -691,6 +721,17 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa }; autocomplete.on_select = [this](unsigned int index, const std::string &text, bool hide_window) { + if(!completion_strings[index]) { // Insert snippet instead + get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); + + if(!hide_window) { + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), text); + } + else + insert_snippet(CompletionDialog::get()->start_mark->get_iter(), snippet_inserts[index]); + return; + } + std::string row; auto pos = text.find(" → "); if(pos != std::string::npos) @@ -779,7 +820,7 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa }; autocomplete.get_tooltip = [this](unsigned int index) { - return clangmm::to_string(clang_getCompletionBriefComment(completion_strings[index])); + return completion_strings[index] ? clangmm::to_string(clang_getCompletionBriefComment(completion_strings[index])) : snippet_comments[index]; }; } diff --git a/src/source_clang.h b/src/source_clang.h index 3634ea5..04e0651 100644 --- a/src/source_clang.h +++ b/src/source_clang.h @@ -65,6 +65,10 @@ namespace Source { std::vector completion_strings; sigc::connection delayed_show_arguments_connection; + std::atomic enable_snippets = {false}; + std::map snippet_inserts; + std::map snippet_comments; + private: std::atomic show_parameters = {false}; diff --git a/src/source_generic.cc b/src/source_generic.cc new file mode 100644 index 0000000..ca0809a --- /dev/null +++ b/src/source_generic.cc @@ -0,0 +1,346 @@ +#include "source_generic.h" +#include "selection_dialog.h" +#include "snippets.h" +#include "terminal.h" + +Source::GenericView::GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), View(file_path, language, true), autocomplete(this, interactive_completion, last_keyval, false) { + configure(); + spellcheck_all = true; + + if(language) { + auto language_manager = LanguageManager::get_default(); + auto search_paths = language_manager->get_search_path(); + bool found_language_file = false; + boost::filesystem::path language_file; + for(auto &search_path : search_paths) { + boost::filesystem::path p(static_cast(search_path) + '/' + static_cast(language->get_id()) + ".lang"); + if(boost::filesystem::exists(p) && boost::filesystem::is_regular_file(p)) { + language_file = p; + found_language_file = true; + break; + } + } + + if(found_language_file) { + boost::property_tree::ptree pt; + try { + boost::property_tree::xml_parser::read_xml(language_file.string(), pt); + } + catch(const std::exception &e) { + Terminal::get().print("Error: error parsing language file " + language_file.string() + ": " + e.what() + '\n', true); + } + bool has_context_class = false; + parse_language_file(has_context_class, pt); + if(!has_context_class || language->get_id() == "cmake") // TODO: no longer use the spellcheck_all flag? + spellcheck_all = false; + } + } + + setup_buffer_words(); + + setup_autocomplete(); +} + +Source::GenericView::~GenericView() { + autocomplete.state = Autocomplete::State::IDLE; + if(autocomplete.thread.joinable()) + autocomplete.thread.join(); +} + +void Source::GenericView::parse_language_file(bool &has_context_class, const boost::property_tree::ptree &pt) { + bool case_insensitive = false; + for(auto &node : pt) { + if(node.first == "") { + auto data = static_cast(node.second.data()); + std::transform(data.begin(), data.end(), data.begin(), ::tolower); + if(data.find("case insensitive") != std::string::npos) + case_insensitive = true; + } + else if(node.first == "keyword") { + auto data = static_cast(node.second.data()); + keywords.emplace(data); + + if(case_insensitive) { + std::transform(data.begin(), data.end(), data.begin(), ::tolower); + keywords.emplace(data); + } + } + else if(!has_context_class && node.first == "context") { + auto class_attribute = node.second.get(".class", ""); + auto class_disabled_attribute = node.second.get(".class-disabled", ""); + if(class_attribute.size() > 0 || class_disabled_attribute.size() > 0) + has_context_class = true; + } + try { + parse_language_file(has_context_class, node.second); + } + catch(const std::exception &e) { + } + } +} + +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(!(*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; + } + iter.forward_char(); + } + + return words; +} + +void Source::GenericView::setup_buffer_words() { + { + auto words = get_words(get_buffer()->begin(), get_buffer()->end()); + std::lock_guard lock(buffer_words_mutex); + for(auto &word : words) { + auto result = buffer_words.emplace(get_buffer()->get_text(word.first, word.second), 1); + if(!result.second) + ++(result.first->second); + } + } + + // 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)) + iter.backward_char(); + + if(is_word_iter(iter)) { + auto word = get_word(iter); + if(word.second.get_offset() - word.first.get_offset() >= 3) { + std::lock_guard lock(buffer_words_mutex); + auto it = buffer_words.find(get_buffer()->get_text(word.first, word.second)); + if(it != buffer_words.end()) { + if(it->second > 1) + --(it->second); + else + buffer_words.erase(it); + } + } + } + }, false); + + // Add all words between start and end of insert + get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &iter, const Glib::ustring &text, int bytes) { + auto start = iter; + auto end = iter; + start.backward_chars(text.size()); + if(!is_word_iter(start)) + start.backward_char(); + end.forward_char(); + + auto words = get_words(start, end); + std::lock_guard lock(buffer_words_mutex); + for(auto &word : words) { + auto result = buffer_words.emplace(get_buffer()->get_text(word.first, word.second), 1); + if(!result.second) + ++(result.first->second); + } + }); + + // Remove words within text that was removed + 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)) + start.backward_char(); + end.forward_char(); + auto words = get_words(start, end); + std::lock_guard lock(buffer_words_mutex); + for(auto &word : words) { + auto it = buffer_words.find(get_buffer()->get_text(word.first, word.second)); + if(it != buffer_words.end()) { + if(it->second > 1) + --(it->second); + else + buffer_words.erase(it); + } + } + }, false); + + // 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)) + start.backward_char(); + if(is_word_iter(start)) { + auto word = get_word(start); + if(word.second.get_offset() - word.first.get_offset() >= 3) { + std::lock_guard lock(buffer_words_mutex); + auto result = buffer_words.emplace(get_buffer()->get_text(word.first, word.second), 1); + if(!result.second) + ++(result.first->second); + } + } + }); +} + +void Source::GenericView::setup_autocomplete() { + non_interactive_completion = [this] { + if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) + return; + autocomplete.run(); + }; + + autocomplete.reparse = [this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + autocomplete.is_continue_key = [](guint keyval) { + if((keyval >= '0' && keyval <= '9') || (keyval >= 'a' && keyval <= 'z') || (keyval >= 'A' && keyval <= 'Z') || keyval == '_' || gdk_keyval_to_unicode(keyval) >= 0x00C0) + return true; + + return false; + }; + + autocomplete.is_restart_key = [](guint keyval) { + return false; + }; + + autocomplete.run_check = [this]() { + auto start = get_buffer()->get_insert()->get_iter(); + auto end = start; + size_t count = 0; + while(start.backward_char() && ((*start >= '0' && *start <= '9') || (*start >= 'a' && *start <= 'z') || (*start >= 'A' && *start <= 'Z') || *start == '_' || *start >= 0x00C0)) + ++count; + if((start.is_start() || start.forward_char()) && count >= 3 && !(*start >= '0' && *start <= '9')) { + std::lock(autocomplete.prefix_mutex, buffer_words_mutex); + std::lock_guard lock1(autocomplete.prefix_mutex, std::adopt_lock); + std::lock_guard lock2(buffer_words_mutex, std::adopt_lock); + autocomplete.prefix = get_buffer()->get_text(start, end); + show_prefix_buffer_word = buffer_words.find(autocomplete.prefix) != buffer_words.end(); + return true; + } + else if(!interactive_completion) { + auto end_iter = get_buffer()->get_insert()->get_iter(); + auto iter = end_iter; + while(iter.backward_char() && autocomplete.is_continue_key(*iter)) { + } + if(iter != end_iter) + iter.forward_char(); + std::lock(autocomplete.prefix_mutex, buffer_words_mutex); + std::lock_guard lock1(autocomplete.prefix_mutex, std::adopt_lock); + std::lock_guard lock2(buffer_words_mutex, std::adopt_lock); + autocomplete.prefix = get_buffer()->get_text(iter, end_iter); + show_prefix_buffer_word = buffer_words.find(autocomplete.prefix) != buffer_words.end(); + return true; + } + + return false; + }; + + autocomplete.before_add_rows = [this] { + status_state = "autocomplete..."; + if(update_status_state) + update_status_state(this); + }; + + autocomplete.after_add_rows = [this] { + status_state = ""; + if(update_status_state) + update_status_state(this); + }; + + autocomplete.on_add_rows_error = [this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + autocomplete.add_rows = [this](std::string &buffer, int line_number, int column) { + if(autocomplete.state == Autocomplete::State::STARTING) { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + + std::string prefix; + { + std::lock_guard lock(autocomplete.prefix_mutex); + prefix = autocomplete.prefix; + } + for(auto &keyword : keywords) { + if(prefix.compare(0, prefix.size(), keyword, 0, prefix.size()) == 0) { + autocomplete.rows.emplace_back(keyword); + autocomplete_insert.emplace_back(keyword); + autocomplete_comment.emplace_back(""); + } + } + bool show_prefix_buffer_word = this->show_prefix_buffer_word; + { + std::lock_guard lock(buffer_words_mutex); + for(auto &buffer_word : buffer_words) { + if((show_prefix_buffer_word || buffer_word.first.size() > prefix.size()) && prefix.compare(0, prefix.size(), buffer_word.first, 0, prefix.size()) == 0 && + keywords.find(buffer_word.first) == keywords.end()) { + autocomplete.rows.emplace_back(buffer_word.first); + autocomplete_insert.emplace_back(buffer_word.first); + autocomplete_comment.emplace_back(""); + } + } + } + std::lock_guard lock(snippets_mutex); + if(snippets) { + for(auto &snippet : *snippets) { + if(prefix.compare(0, prefix.size(), snippet.prefix, 0, prefix.size()) == 0) { + autocomplete.rows.emplace_back(snippet.prefix); + autocomplete_insert.emplace_back(snippet.body); + autocomplete_comment.emplace_back(snippet.description); + } + } + } + } + }; + + autocomplete.on_show = [this] { + hide_tooltips(); + }; + + autocomplete.on_hide = [this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + autocomplete.on_select = [this](unsigned int index, const std::string &text, bool hide_window) { + Glib::ustring insert = hide_window ? autocomplete_insert[index] : text; + + get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); + + if(hide_window) + insert_snippet(CompletionDialog::get()->start_mark->get_iter(), insert); + else + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); + }; + + autocomplete.get_tooltip = [this](unsigned int index) { + return autocomplete_comment[index]; + }; +} diff --git a/src/source_generic.h b/src/source_generic.h new file mode 100644 index 0000000..7b1101a --- /dev/null +++ b/src/source_generic.h @@ -0,0 +1,31 @@ +#pragma once + +#include "autocomplete.h" +#include "source.h" +#include + +namespace Source { + class GenericView : public View { + public: + GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language); + ~GenericView(); + + private: + void parse_language_file(bool &has_context_class, const boost::property_tree::ptree &pt); + + 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); + + std::map buffer_words; + void setup_buffer_words(); + std::atomic show_prefix_buffer_word = {false}; /// To avoid showing the current word if it is unique in document + + Autocomplete autocomplete; + std::vector autocomplete_comment; + std::vector autocomplete_insert; + void setup_autocomplete(); + }; +} // namespace Source diff --git a/src/source_language_protocol.cc b/src/source_language_protocol.cc index f9fd257..016fe75 100644 --- a/src/source_language_protocol.cc +++ b/src/source_language_protocol.cc @@ -1251,6 +1251,7 @@ void Source::LanguageProtocolView::setup_autocomplete() { if(!is_code_iter(iter)) return false; + autocomplete_enable_snippets = false; autocomplete_show_parameters = false; auto line = ' ' + get_line_before(); @@ -1262,6 +1263,8 @@ void Source::LanguageProtocolView::setup_autocomplete() { { std::unique_lock lock(autocomplete.prefix_mutex); autocomplete.prefix = sm.length(2) ? sm[3].str() : sm.length(4) ? sm[5].str() : sm[6].str(); + if(!sm.length(2) && !sm.length(4)) + autocomplete_enable_snippets = true; } return true; } @@ -1278,8 +1281,20 @@ void Source::LanguageProtocolView::setup_autocomplete() { } if(iter != end_iter) iter.forward_char(); - std::unique_lock lock(autocomplete.prefix_mutex); - autocomplete.prefix = get_buffer()->get_text(iter, end_iter); + + { + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix = get_buffer()->get_text(iter, end_iter); + } + auto prev1 = iter; + if(prev1.backward_char() && *prev1 != '.') { + auto prev2 = prev1; + if(!prev2.backward_char()) + autocomplete_enable_snippets = true; + else if(!(*prev2 == ':' && *prev1 == ':')) + autocomplete_enable_snippets = true; + } + return true; } @@ -1382,6 +1397,24 @@ void Source::LanguageProtocolView::setup_autocomplete() { autocomplete_insert.emplace_back(std::move(insert)); } } + + if(autocomplete_enable_snippets) { + std::string prefix; + { + std::unique_lock lock(autocomplete.prefix_mutex); + prefix = autocomplete.prefix; + } + std::lock_guard lock(snippets_mutex); + if(snippets) { + for(auto &snippet : *snippets) { + if(prefix.compare(0, prefix.size(), snippet.prefix, 0, prefix.size()) == 0) { + autocomplete.rows.emplace_back(snippet.prefix); + autocomplete_insert.emplace_back(snippet.body); + autocomplete_comment.emplace_back(snippet.description); + } + } + } + } } result_processed.set_value(); }); @@ -1390,44 +1423,7 @@ void Source::LanguageProtocolView::setup_autocomplete() { } }; - signal_key_press_event().connect([this](GdkEventKey *event) { - if((event->keyval == GDK_KEY_Tab || event->keyval == GDK_KEY_ISO_Left_Tab) && (event->state & GDK_SHIFT_MASK) == 0) { - if(!argument_marks.empty()) { - auto it = argument_marks.begin(); - auto start = it->first->get_iter(); - auto end = it->second->get_iter(); - if(start == end) - return false; - keep_argument_marks = true; - get_buffer()->select_range(it->first->get_iter(), it->second->get_iter()); - keep_argument_marks = false; - get_buffer()->delete_mark(it->first); - get_buffer()->delete_mark(it->second); - argument_marks.erase(it); - return true; - } - } - return false; - }, false); - - get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator &iterator, const Glib::RefPtr &mark) { - if(mark->get_name() == "insert") { - if(!keep_argument_marks) { - for(auto &pair : argument_marks) { - get_buffer()->delete_mark(pair.first); - get_buffer()->delete_mark(pair.second); - } - argument_marks.clear(); - } - } - }); - autocomplete.on_show = [this] { - for(auto &pair : argument_marks) { - get_buffer()->delete_mark(pair.first); - get_buffer()->delete_mark(pair.second); - } - argument_marks.clear(); hide_tooltips(); }; @@ -1452,56 +1448,18 @@ void Source::LanguageProtocolView::setup_autocomplete() { if(hide_window) { if(autocomplete_show_parameters) { - if(has_named_parameters()) { // Do not select named parameters in for instance Python + if(has_named_parameters()) // Do not select named parameters in for instance Python get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); - return; - } else { get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); int start_offset = CompletionDialog::get()->start_mark->get_iter().get_offset(); int end_offset = CompletionDialog::get()->start_mark->get_iter().get_offset() + insert.size(); get_buffer()->select_range(get_buffer()->get_iter_at_offset(start_offset), get_buffer()->get_iter_at_offset(end_offset)); - return; } + return; } - // Find and add position marks that one can move to using tab-key - size_t pos1 = 0; - std::vector> mark_offsets; - while((pos1 = insert.find("${"), pos1) != Glib::ustring::npos) { - size_t pos2 = insert.find(":", pos1 + 2); - if(pos2 != Glib::ustring::npos) { - size_t pos3 = insert.find("}", pos2 + 1); - if(pos3 != Glib::ustring::npos) { - size_t length = pos3 - pos2 - 1; - insert.erase(pos3, 1); - insert.erase(pos1, pos2 - pos1 + 1); - mark_offsets.emplace_back(pos1, pos1 + length); - pos1 += length; - } - else - break; - } - else - break; - } - get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); - for(auto &offset : mark_offsets) { - auto start = CompletionDialog::get()->start_mark->get_iter(); - auto end = start; - start.forward_chars(offset.first); - end.forward_chars(offset.second); - argument_marks.emplace_back(get_buffer()->create_mark(start), get_buffer()->create_mark(end)); - } - if(!argument_marks.empty()) { - auto it = argument_marks.begin(); - keep_argument_marks = true; - get_buffer()->select_range(it->first->get_iter(), it->second->get_iter()); - keep_argument_marks = false; - get_buffer()->delete_mark(it->first); - get_buffer()->delete_mark(it->second); - argument_marks.erase(it); - } + insert_snippet(CompletionDialog::get()->start_mark->get_iter(), insert); } else get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); diff --git a/src/source_language_protocol.h b/src/source_language_protocol.h index 67a4411..6872187 100644 --- a/src/source_language_protocol.h +++ b/src/source_language_protocol.h @@ -2,6 +2,7 @@ #include "autocomplete.h" #include "process.hpp" #include "source.h" +#include #include #include #include @@ -162,7 +163,7 @@ namespace Source { void setup_autocomplete(); std::vector autocomplete_comment; std::vector autocomplete_insert; - std::list, Glib::RefPtr>> argument_marks; + std::atomic autocomplete_enable_snippets = {false}; bool autocomplete_show_parameters = false; sigc::connection autocomplete_delayed_show_arguments_connection; diff --git a/src/window.cc b/src/window.cc index 9d83d4a..6ec5767 100644 --- a/src/window.cc +++ b/src/window.cc @@ -191,6 +191,9 @@ void Window::set_menu_actions() { menu.add_action("preferences", []() { Notebook::get().open(Config::get().home_juci_path / "config" / "config.json"); }); + menu.add_action("snippets", []() { + Notebook::get().open(Config::get().home_juci_path / "snippets.json"); + }); menu.add_action("quit", [this]() { close(); }); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e873016..833ef76 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -46,6 +46,10 @@ add_executable(source_clang_test source_clang_test.cc $) +target_link_libraries(source_generic_test juci_shared) +add_test(source_generic_test source_generic_test) + add_executable(source_key_test source_key_test.cc $) target_link_libraries(source_key_test juci_shared) add_test(source_key_test source_key_test) diff --git a/tests/source_generic_test.cc b/tests/source_generic_test.cc new file mode 100644 index 0000000..71d33a7 --- /dev/null +++ b/tests/source_generic_test.cc @@ -0,0 +1,143 @@ +#include "source_generic.h" +#include + +int main() { + auto app = Gtk::Application::create(); + Gsv::init(); + + auto tests_path = boost::filesystem::canonical(JUCI_TESTS_PATH); + auto source_file = tests_path / "tmp" / "source_file.md"; + + auto language_manager = Gsv::LanguageManager::get_default(); + + auto language = language_manager->get_language("markdown"); + Source::GenericView view(source_file, language); + + // Test buffer words + { + std::map buffer_words = {}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->set_text("testing 1 2 3"); + buffer_words = {{"testing", 1}}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->set_text(""); + assert(view.buffer_words.empty()); + + view.get_buffer()->set_text("\ntest ing te\n"); + buffer_words = {{"test", 1}, {"ing", 1}}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->set_text("test ing\ntest ing te\n"); + buffer_words = {{"test", 2}, {"ing", 2}}; + assert(view.buffer_words == buffer_words); + + auto start = view.get_buffer()->begin(); + start.forward_chars(4); + auto end = start; + end.forward_char(); + view.get_buffer()->erase(start, end); + buffer_words = {{"testing", 1}, {"test", 1}, {"ing", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(4); + view.get_buffer()->insert(start, " "); + buffer_words = {{"test", 2}, {"ing", 2}}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->erase(view.get_buffer()->begin(), view.get_buffer()->end()); + assert(view.buffer_words.empty()); + + view.get_buffer()->set_text("this is this"); + buffer_words = {{"this", 2}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(4); + end = start; + end.forward_char(); + view.get_buffer()->erase(start, end); + buffer_words = {{"thisis", 1}, {"this", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(4); + view.get_buffer()->insert(start, "\n\n"); + buffer_words = {{"this", 2}}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->set_text("test"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + end = start; + end.forward_chars(2); + view.get_buffer()->erase(start, end); + assert(view.buffer_words.empty()); + + start = view.get_buffer()->begin(); + view.get_buffer()->insert(start, "te"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(2); + end = start; + end.forward_chars(2); + view.get_buffer()->erase(start, end); + assert(view.buffer_words.empty()); + + start = view.get_buffer()->begin(); + start.forward_chars(2); + view.get_buffer()->insert(start, "st"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + view.get_buffer()->set_text("\ntest\n"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_char(); + end = start; + end.forward_chars(2); + view.get_buffer()->erase(start, end); + assert(view.buffer_words.empty()); + + start = view.get_buffer()->begin(); + start.forward_char(); + view.get_buffer()->insert(start, "te"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(3); + end = start; + end.forward_chars(2); + view.get_buffer()->erase(start, end); + assert(view.buffer_words.empty()); + + start = view.get_buffer()->begin(); + start.forward_chars(3); + view.get_buffer()->insert(start, "st"); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(2); + view.get_buffer()->insert(start, "ee"); + buffer_words = {{"teeest", 1}}; + assert(view.buffer_words == buffer_words); + + start = view.get_buffer()->begin(); + start.forward_chars(2); + end = start; + end.forward_chars(2); + view.get_buffer()->erase(start, end); + buffer_words = {{"test", 1}}; + assert(view.buffer_words == buffer_words); + } +} \ No newline at end of file