From 0f87493593f40b7ff73ea201a3da271556e89235 Mon Sep 17 00:00:00 2001 From: eidheim Date: Mon, 28 Oct 2019 12:00:49 +0100 Subject: [PATCH] Improved snippets: now supports variables TM_SELECTED_TEXT, TM_CURRENT_LINE, TM_CURRENT_WORD, TM_LINE_INDEX, TM_LINE_NUMBER, TM_FILENAME_BASE, TM_FILENAME, TM_DIRECTORY, TM_FILEPATH, CLIPBOARD, and default values. Binding shortcuts to snippets is also possible by adding key values in snippets.json --- README.md | 2 +- src/snippets.cc | 14 +++- src/snippets.h | 3 + src/source.cc | 12 +++ src/source_base.cc | 197 ++++++++++++++++++++++++++++++++++++++++----- src/source_base.h | 2 +- 6 files changed, 204 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 87220db..c699604 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ towards libclang with speed, stability, and ease of use in mind. * 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. +* Snippets can be added in ~/.juci/snippets.json using the [TextMate snippet syntax](https://macromates.com/manual/en/snippets). 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/snippets.cc b/src/snippets.cc index daadc2b..bd01553 100644 --- a/src/snippets.cc +++ b/src/snippets.cc @@ -16,6 +16,7 @@ void Snippets::load() { filesystem::write(snippets_file, R"({ "^markdown$": [ { + "key": "1", "prefix": "code_block", "body": "```${1:language}\n${2:code}\n```\n", "description": "Insert code block" @@ -30,8 +31,17 @@ void Snippets::load() { 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", "")}); + for(auto snippet_it = language_it->second.begin(); snippet_it != language_it->second.end(); ++snippet_it) { + auto key_string = snippet_it->second.get("key", ""); + guint key = 0; + GdkModifierType modifier = static_cast(0); + if(!key_string.empty()) { + gtk_accelerator_parse(key_string.c_str(), &key, &modifier); + if(key == 0 && modifier == 0) + Terminal::get().async_print("Error: could not parse key string: " + key_string + "\n", true); + } + snippets.back().second.emplace_back(Snippet{snippet_it->second.get("prefix", ""), key, modifier, snippet_it->second.get("body"), snippet_it->second.get("description", "")}); + } } } catch(const std::exception &e) { diff --git a/src/snippets.h b/src/snippets.h index 456dc72..8e8f204 100644 --- a/src/snippets.h +++ b/src/snippets.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -11,6 +12,8 @@ public: class Snippet { public: std::string prefix; + guint key; + GdkModifierType modifier; std::string body; std::string description; }; diff --git a/src/source.cc b/src/source.cc index ca3cbe6..e81b4ce 100644 --- a/src/source.cc +++ b/src/source.cc @@ -2092,6 +2092,18 @@ bool Source::View::on_key_press_event(GdkEventKey *key) { else if(Config::get().source.enable_multiple_cursors && on_key_press_event_extra_cursors(key)) return true; + { + LockGuard lock(snippets_mutex); + if(snippets) { + for(auto &snippet : *snippets) { + if(snippet.key == key->keyval && (snippet.modifier & key->state) == snippet.modifier) { + insert_snippet(get_buffer()->get_insert()->get_iter(), snippet.body); + return true; + } + } + } + } + //Move cursor one paragraph down if((key->keyval == GDK_KEY_Down || key->keyval == GDK_KEY_KP_Down) && (key->state & GDK_CONTROL_MASK) > 0) { auto selection_start_iter = get_buffer()->get_selection_bound()->get_iter(); diff --git a/src/source_base.cc b/src/source_base.cc index 724690d..f61d168 100644 --- a/src/source_base.cc +++ b/src/source_base.cc @@ -501,8 +501,6 @@ Gtk::TextIter Source::BaseView::get_iter_at_line_end(int line_nr) { else if(line_nr + 1 < get_buffer()->get_line_count()) { auto iter = get_buffer()->get_iter_at_line(line_nr + 1); iter.backward_char(); - if(!iter.ends_line()) // for CR+LF - iter.backward_char(); return iter; } else { @@ -1082,32 +1080,187 @@ void Source::BaseView::setup_extra_cursor_signals() { }); } -void Source::BaseView::insert_snippet(Gtk::TextIter iter, Glib::ustring snippet) { +void Source::BaseView::insert_snippet(Gtk::TextIter iter, const 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(...) { + + Glib::ustring insert; + insert.reserve(snippet.size()); + size_t i = 0; + int number; + auto parse_number = [&snippet, &i, &number]() { + if(i >= snippet.size()) + throw std::out_of_range("unexpected end"); + std::string str; + for(; i < snippet.size() && snippet[i] >= '0' && snippet[i] <= '9'; ++i) + str += snippet[i]; + if(str.empty()) + return false; + try { + number = std::stoi(str); + return true; + } + catch(...) { + return false; + } + }; + bool erase_line = false, erase_word = false; + auto parse_variable = [this, &iter, &snippet, &i, &insert, &erase_line, &erase_word] { + if(i >= snippet.size()) + throw std::out_of_range("unexpected end"); + if(snippet.compare(i, 16, "TM_SELECTED_TEXT") == 0) { + i += 16; + Gtk::TextIter start, end; + if(get_buffer()->get_selection_bounds(start, end)) { + insert += get_buffer()->get_text(start, end); + return true; + } + return false; + } + else if(snippet.compare(i, 15, "TM_CURRENT_LINE") == 0) { + i += 15; + auto start = get_buffer()->get_iter_at_line(iter.get_line()); + auto end = get_iter_at_line_end(iter.get_line()); + insert += get_buffer()->get_text(start, end); + erase_line = true; + return true; + } + else if(snippet.compare(i, 15, "TM_CURRENT_WORD") == 0) { + i += 15; + if(is_token_char(*iter)) { + auto token_iters = get_token_iters(iter); + insert += get_buffer()->get_text(token_iters.first, token_iters.second); + erase_word = true; + return true; + } + return false; + } + else if(snippet.compare(i, 13, "TM_LINE_INDEX") == 0) { + i += 13; + insert += std::to_string(iter.get_line()); + return true; + } + else if(snippet.compare(i, 14, "TM_LINE_NUMBER") == 0) { + i += 14; + insert += std::to_string(iter.get_line() + 1); + return true; + } + else if(snippet.compare(i, 16, "TM_FILENAME_BASE") == 0) { + i += 16; + insert += file_path.stem().string(); + return true; + } + else if(snippet.compare(i, 11, "TM_FILENAME") == 0) { + i += 11; + insert += file_path.filename().string(); + return true; + } + else if(snippet.compare(i, 12, "TM_DIRECTORY") == 0) { + i += 12; + insert += file_path.parent_path().string(); + return true; + } + else if(snippet.compare(i, 11, "TM_FILEPATH") == 0) { + i += 11; + insert += file_path.string(); + return true; + } + else if(snippet.compare(i, 9, "CLIPBOARD") == 0) { + i += 9; + insert += Gtk::Clipboard::get()->wait_for_text(); + return true; + } + // TODO: support other variables + return false; + }; + try { + bool escape = false; + while(i < snippet.size()) { + if(escape) { + insert += snippet[i]; + escape = false; + ++i; + } + else if(snippet[i] == '\\') { + escape = true; + ++i; + } + else if(snippet[i] == '$') { + ++i; + if(snippet.at(i) == '{') { + ++i; + if(parse_number()) { + Glib::ustring placeholder; + if(snippet.at(i) == ':') { + ++i; + for(; snippet.at(i) != '}'; ++i) + placeholder += snippet[i]; + } + if(snippet.at(i) != '}') + throw std::logic_error("closing } not found"); + ++i; + arguments_offsets[number].emplace_back(insert.size(), insert.size() + placeholder.size()); + insert += placeholder; + } + else { + if(!parse_variable()) { + if(snippet.at(i) == ':') { // Use default value + ++i; + parse_variable(); + } + } + else if(snippet.at(i) == ':') { // Skip default value + while(snippet.at(++i) != '}') { + } + } + if(snippet.at(i) != '}') + throw std::logic_error("closing } not found"); + ++i; + } } + else if(parse_number()) + arguments_offsets[number].emplace_back(insert.size(), insert.size()); + else + parse_variable(); } + else + insert += snippet[i++]; } - break; + } + catch(...) { + Terminal::get().print("Error: could not parse snippet: " + snippet + '\n', true); + return; + } + + // $0 should be last argument + auto it = arguments_offsets.find(0); + if(it != arguments_offsets.end()) { + auto rit = arguments_offsets.rbegin(); + arguments_offsets.emplace(rit->first + 1, std::move(it->second)); + arguments_offsets.erase(it); } auto mark = get_buffer()->create_mark(iter); - get_buffer()->insert(iter, snippet); + + get_source_buffer()->begin_user_action(); + Gtk::TextIter start, end; + if(get_buffer()->get_selection_bounds(start, end)) { + get_buffer()->erase(start, end); + iter = mark->get_iter(); + } + else if(erase_line) { + auto start = get_buffer()->get_iter_at_line(iter.get_line()); + auto end = get_iter_at_line_end(iter.get_line()); + get_buffer()->erase(start, end); + iter = mark->get_iter(); + } + else if(erase_word && is_token_char(*iter)) { + auto token_iters = get_token_iters(iter); + get_buffer()->erase(token_iters.first, token_iters.second); + iter = mark->get_iter(); + } + get_buffer()->insert(iter, insert); + get_source_buffer()->end_user_action(); + 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) { @@ -1117,7 +1270,7 @@ void Source::BaseView::insert_snippet(Gtk::TextIter iter, Glib::ustring snippet) 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)); + snippets_marks.front().emplace_back(get_buffer()->create_mark(start, false), get_buffer()->create_mark(end, false)); get_buffer()->apply_tag(snippet_argument_tag, start, end); } } diff --git a/src/source_base.h b/src/source_base.h index 2a977d0..548a1f2 100644 --- a/src/source_base.h +++ b/src/source_base.h @@ -144,7 +144,7 @@ namespace Source { std::vector *snippets GUARDED_BY(snippets_mutex) = nullptr; std::list, Glib::RefPtr>>> snippets_marks; Glib::RefPtr snippet_argument_tag; - void insert_snippet(Gtk::TextIter iter, Glib::ustring snippet); + void insert_snippet(Gtk::TextIter iter, const Glib::ustring &snippet); bool select_snippet_argument(); bool clear_snippet_marks(); };