#include "editor.h" #include "transform.h" #include "indent.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace ng { static std::map& editors () { static std::map editors; return editors; } editor_ptr editor_for_document (document::document_ptr document) { static struct document_close_callback_t : document::document_t::callback_t { WATCH_LEAKS(document_close_callback_t); document_close_callback_t () { } void handle_document_event (document::document_ptr document, event_t event) { if(event == did_change_open_status && !document->is_open()) { document->remove_callback(this); editors().erase(document->identifier()); } else if(event == did_change_content) { for(auto pair : editors()) pair.second->sanitize_selection(); } } } callback; std::map::iterator editor = editors().find(document->identifier()); if(editor == editors().end()) { document->add_callback(&callback); editor = editors().emplace(document->identifier(), std::make_shared(document)).first; } return editor->second; } static std::string sanitized_utf8 (std::string str) { str.erase(utf8::remove_malformed(str.begin(), str.end()), str.end()); return str; } static find::options_t convert (std::map const& options) { static struct { std::string key; find::options_t flag; } const map[] = { { "fullWordMatch", find::full_words }, { "ignoreCase", find::ignore_case }, { "ignoreWhitespace", find::ignore_whitespace }, { "regularExpression", find::regular_expression }, { "wrapAround", find::wrap_around }, }; find::options_t res = find::none; for(size_t i = 0; i < sizeofA(map); ++i) { std::map::const_iterator it = options.find(map[i].key); if(it != options.end() && it->second == "1") res = res | map[i].flag; } return res; } template _OutputIter transpose_selections (buffer_t const& _buffer, ranges_t const& _selections, _OutputIter out) { ranges_t sel; for(auto const& range : _selections) { size_t from = range.min().index, to = range.max().index; if(from == to) { text::pos_t const& pos = _buffer.convert(from); if(from == 0 || from == _buffer.size()) { } else if(pos.column == 0) { from = _buffer.begin(pos.line - 1); to = pos.line+1 == _buffer.lines() ? _buffer.size() : _buffer.begin(pos.line + 1); } else if(from == _buffer.eol(pos.line)) { from = _buffer.begin(pos.line); to = pos.line+2 == _buffer.lines() ? _buffer.size() : _buffer.begin(pos.line + 2); } else { from = from - _buffer[from-1].size(); to = to + _buffer[to].size(); } *out++ = std::make_pair(range_t(from, to), transform::transpose(_buffer.substr(from, to))); } else if(range.columnar) // TODO from.line != to.line { std::vector strings; std::vector ranges; for(auto const& r : dissect_columnar(_buffer, range)) { strings.push_back(_buffer.substr(r.min().index, r.max().index)); ranges.push_back(r); } for(size_t i = 0; i < ranges.size(); ++i) *out++ = std::make_pair(ranges[i], strings[ranges.size()-1 - i]); } else { *out++ = std::make_pair(range_t(from, to), transform::transpose(_buffer.substr(from, to))); } } return out; } // ============================= // = Preserve Selection Helper = // ============================= static size_t const kColumnar = 1 << 0; static size_t const kReversed = 1 << 1; struct preserve_selection_helper_t : callback_t { preserve_selection_helper_t (buffer_t& buffer, ranges_t const& marks) : _buffer(buffer) { for(auto const& range : marks) { if(range.empty()) { _marks.emplace_back(range.first, mark_t::kUnpairedMark); _marks.emplace_back(range.first, mark_t::kEndMark); } else { size_t userInfo = (range.columnar ? kColumnar : 0) | (range.last < range.first ? kReversed : 0); _marks.emplace_back(range.min(), mark_t::kBeginMark, userInfo); _marks.emplace_back(range.max(), mark_t::kEndMark, userInfo); } } _buffer.add_callback(this); } ~preserve_selection_helper_t () { _buffer.remove_callback(this); } ranges_t get () { ranges_t sel; for(size_t i = 0; i < _marks.size(); i += 2) { ASSERT(i+1 < _marks.size() && _marks[i+1].type == mark_t::kEndMark); if(_marks[i].type == mark_t::kUnpairedMark) { sel.push_back(_marks[i].position); } else { index_t first = _marks[i].position; index_t last = _marks[i+1].position; bool columnar = _marks[i].user_info & kColumnar; if(_marks[i].user_info & kReversed) std::swap(first, last); sel.push_back(range_t(first, last, columnar)); } } return sel; } void will_replace (size_t from, size_t to, std::string const& str) { for(auto& mark : _marks) { size_t& index = mark.position.index; if(oak::cap(from, index, to) == index) { if(mark.type == mark_t::kUnpairedMark || index != from && index != to) { index = from + str.size() - std::min(to - index, str.size()); } else { index = from; if(mark.type == mark_t::kEndMark) index += str.size(); } } else if(from < index) { ASSERT_LT(to, index); index = index + str.size() - (to - from); } } } private: struct mark_t { index_t position; enum mark_type { kBeginMark, kUnpairedMark, kEndMark } type; size_t user_info; mark_t (index_t const& position, mark_type type, size_t user_info = 0) : position(position), type(type), user_info(user_info) { } }; buffer_t& _buffer; std::vector _marks; }; // ======================================================= // = Transform ranges in buffer, accounting for snippets = // ======================================================= template std::multimap map (ng::buffer_t const& buffer, ng::ranges_t const& selections, F op) { std::multimap replacements; for(auto const& range : dissect_columnar(buffer, selections)) replacements.emplace(range, op(buffer.substr(range.min().index, range.max().index))); return replacements; } static ranges_t replace_helper (ng::buffer_t& buffer, snippet_controller_t& snippets, std::multimap const& replacements) { ranges_t res; ssize_t adjustment = 0; for(auto const& p1 : replacements) { range_t orgRange = p1.first.sorted(); if(orgRange.first.index == orgRange.last.index && p1.second.empty()) { res.push_back(orgRange + adjustment); continue; } std::string const pad = orgRange.freehanded && orgRange.first.carry ? std::string(orgRange.first.carry, ' ') : ""; orgRange.first.carry = orgRange.last.carry = 0; ng::range_t original(orgRange.first.index + pad.size(), orgRange.first.index + pad.size() + p1.second.size()); bool goodCaretMatch = false; std::vector< std::pair > const& real = snippets.replace(orgRange.first.index, orgRange.last.index, pad + p1.second); for(auto const& p2 : real) { range_t const& range = p2.first; std::string const& str = p2.second; size_t from = range.first.index + adjustment, to = range.last.index + adjustment; size_t caret = buffer.replace(from, to, str); if(snippets.empty() && range == ng::range_t(orgRange.first.index, orgRange.last.index)) { original = range_t(from + pad.size(), caret, false, orgRange.freehanded, true); } else if(!snippets.empty() && !goodCaretMatch && snippets.current().min().index <= from + pad.size() && caret <= snippets.current().max().index) { // FIXME This is a heuristic to figure out which of the returned ranges correspond to our caret, as multiple ranges will be returned when the snippet has mirrors. Ideally we would update the API to return the new caret range. Though I am postponing that to a potential refactor that would also allow snippets to work with multiple carets. original = ng::range_t(from + pad.size(), caret, false, orgRange.freehanded, true); goodCaretMatch = snippets.current().min().index < from + pad.size() && caret < snippets.current().max().index || (from + pad.size() != caret && (snippets.current().min().index < from + pad.size() || caret < snippets.current().max().index)); } adjustment += str.size() - (to - from); } res.push_back(original); res.last().color = orgRange.color; } return res; } template ng::ranges_t apply (ng::buffer_t& buffer, ng::ranges_t const& selections, snippet_controller_t& snippets, F op) { ng::ranges_t res; for(auto const& range : replace_helper(buffer, snippets, map(buffer, selections, op))) res.push_back(range.color ? range : range.max()); return res; } // ============ // = editor_t = // ============ static ng::buffer_t dummy; void editor_t::setup () { set_clipboard(create_simple_clipboard()); set_find_clipboard(create_simple_clipboard()); set_replace_clipboard(create_simple_clipboard()); set_yank_clipboard(create_simple_clipboard()); } editor_t::editor_t () : _buffer(dummy) { setup(); } editor_t::editor_t (buffer_t& buffer) : _buffer(buffer) { setup(); } editor_t::editor_t (document::document_ptr document) : _buffer(document->buffer()), _document(document) { ASSERT(document->is_open()); setup(); } void editor_t::sanitize_selection () { _selections = ng::sanitize(_buffer, _selections); } struct my_clipboard_entry_t : clipboard_t::entry_t { my_clipboard_entry_t (std::string const& content, std::string const& indent, bool complete, size_t fragments, bool columnar) : clipboard_t::entry_t(content) { if(indent != NULL_STR) _options["indent"] = indent; if(complete) _options["complete"] = "1"; if(fragments > 1) _options["fragments"] = std::to_string(fragments); if(columnar) _options["columnar"] = "1"; } std::map const& options () const { return _options; } private: std::map _options; }; clipboard_t::entry_ptr editor_t::copy (ng::buffer_t const& buffer, ng::ranges_t const& selections) { std::string indent = NULL_STR; bool complete = false; if(selections.size() == 1) { range_t const& sel = selections.last(); text::pos_t const& from = buffer.convert(sel.min().index); text::pos_t const& to = buffer.convert(sel.max().index); if(from.line != to.line) { if(from.column != 0) { std::string const& leading = buffer.substr(buffer.begin(from.line), sel.min().index); if(text::is_blank(leading.data(), leading.data() + leading.size())) indent = leading; } if(to.column != 0 && sel.max().index == buffer.eol(to.line)) complete = true; } } std::vector v; for(auto const& range : dissect_columnar(buffer, selections)) v.push_back(buffer.substr(range.min().index, range.max().index)); bool columnar = selections.size() == 1 && selections.last().columnar; return std::make_shared(text::join(v, "\n"), indent, complete, v.size(), columnar); } static bool suitable_for_reindent (std::string const& str) { return oak::contains(str.begin(), str.end(), '\n'); } // ============ // = Snippets = // ============ bool editor_t::disallow_tab_expansion () const { if(!_snippets.empty() && _snippets.current() == ranges().last() && !_snippets.in_last_placeholder() || ranges().last().unanchored) return true; return false; } ranges_t editor_t::replace (std::multimap const& replacements, bool selectInsertions) { ranges_t res = replace_helper(_buffer, _snippets, replacements); return selectInsertions ? res : ng::move(_buffer, res, kSelectionMoveToEndOfSelection); } ranges_t editor_t::snippet (size_t from, size_t to, std::string const& str, std::map const& variables, bool disableIndent) { struct callback_t : snippet::run_command_callback_t { std::string run_command (std::string const& cmd, std::map const& environment) { __block int status = 0; __block std::string output, error; std::string script = cmd; command::fix_shebang(&script); std::string scriptPath = path::temp("snippet_command", script); if(io::process_t process = io::spawn(std::vector{ scriptPath }, environment)) { close(process.in); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ io::exhaust_fd(process.out, &output); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ io::exhaust_fd(process.err, &error); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if(waitpid(process.pid, &status, 0) != process.pid) perror("waitpid"); }); dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_release(group); } unlink(scriptPath.c_str()); std::string const& res = WIFEXITED(status) && WEXITSTATUS(status) == 0 ? output : error; return utf8::is_valid(res.begin(), res.end()) ? res : sanitized_utf8(res); } } callback; std::string indent = disableIndent ? "" : _buffer.substr(_buffer.begin(_buffer.convert(from).line), from); size_t i = 0; while(i < indent.size() && text::is_space(indent[i])) ++i; indent.resize(i); snippet::snippet_t const& snippet = snippet::parse(str, variables, indent, _buffer.indent(), &callback); std::multimap map; map.emplace(range_t(from, to), snippet.text); _snippets.push(snippet, this->replace(map, true).last()); return _snippets.current(); } void editor_t::clear_snippets () { _snippets.clear(); } std::vector const& editor_t::choices () const { return _snippets.choices(); } std::string editor_t::placeholder_content (ng::range_t* placeholderSelection) const { if(_snippets.empty()) return NULL_STR; ng::range_t range = _snippets.current(); for(auto const& r : _selections) { if(placeholderSelection && range.min() <= r.min() && r.max() <= range.max()) *placeholderSelection = ng::range_t(r.min().index - range.min().index, r.max().index - range.min().index); } return _buffer.substr(range.min().index, range.max().index); } void editor_t::set_placeholder_content (std::string const& str, size_t selectFrom) { std::multimap map; map.emplace(_snippets.current(), str); ng::ranges_t res = this->replace(map, true); for(auto& range : res) range.min().index += selectFrom; _selections = res; } // ============ ng::ranges_t editor_t::paste (ng::buffer_t& buffer, ng::ranges_t const& selections, snippet_controller_t& snippets, clipboard_t::entry_ptr entry) { if(!entry) return selections; std::string str = entry->content(); std::map options = entry->options(); str.erase(text::convert_line_endings(str.begin(), str.end(), text::estimate_line_endings(str.begin(), str.end())), str.end()); std::string const& indent = options["indent"]; bool const complete = options["complete"] == "1"; size_t const fragments = strtol(options["fragments"].c_str(), NULL, 10); bool const columnar = options["columnar"] == "1"; if((selections.size() != 1 || selections.last().columnar) && (fragments > 1 || oak::contains(str.begin(), str.end(), '\n'))) { std::vector words = text::split(str, "\n"); if(words.size() > 1 && words.back().empty()) words.pop_back(); size_t i = 0; std::multimap insertions; for(auto const& range : dissect_columnar(buffer, selections)) insertions.emplace(range, words[i++ % words.size()]); return ng::move(buffer, replace_helper(buffer, snippets, insertions), kSelectionMoveToEndOfSelection); } if(fragments > 1 && selections.size() == 1) { ASSERT(fragments == std::count(str.begin(), str.end(), '\n') + 1); if(columnar) { index_t caret = dissect_columnar(buffer, selections).last().min(); size_t n = buffer.convert(caret.index).line; size_t col = visual_distance(buffer, buffer.begin(n), caret); std::multimap insertions; for(auto const& line : text::tokenize(str.begin(), str.end(), '\n')) { if(n+1 < buffer.lines()) { insertions.emplace(visual_advance(buffer, buffer.begin(n), col), line); } else if(n < buffer.lines()) { // we special-case this to ensure we do not insert at last line with carry, as that will cause potential following insertions to have a lower index, since those will be at EOB w/o a carry index_t pos = visual_advance(buffer, buffer.begin(n), col); insertions.emplace(index_t(pos.index), std::string(pos.carry, ' ') + line); } else { insertions.emplace(index_t(buffer.size()), "\n" + std::string(col, ' ') + line); } ++n; } return ng::move(buffer, replace_helper(buffer, snippets, insertions), kSelectionMoveToEndOfSelection).first(); } else { std::multimap insertions; ng::range_t caret = dissect_columnar(buffer, selections).last(); for(auto const& line : text::tokenize(str.begin(), str.end(), '\n')) { insertions.emplace(caret, line); caret = caret.max(); } return ng::move(buffer, replace_helper(buffer, snippets, insertions), kSelectionMoveToEndOfSelection); } } if(selections.size() == 1 && suitable_for_reindent(str)) { size_t const index = selections.last().min().index; size_t const line = buffer.convert(index).line; std::string const& leftOfCaret = buffer.substr(buffer.begin(line), index); if(text::is_blank(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size())) { size_t const tabSize = buffer.indent().tab_size(); size_t const indentSize = buffer.indent().indent_size(); if(indent != NULL_STR) str = indent + str; if(complete) { size_t const rightIndex = selections.last().max().index; size_t const lastLine = buffer.convert(rightIndex).line; std::string const& rightOfCaret = buffer.substr(rightIndex, buffer.eol(lastLine)); if(!text::is_blank(rightOfCaret.data(), rightOfCaret.data() + rightOfCaret.size())) str += '\n'; } int minIndent = INT_MAX; std::vector< std::pair > const& v = text::to_lines(str.data(), str.data() + str.size()); for(auto const& it : v) { if(!text::is_blank(it.first, it.second)) minIndent = std::min(indent::leading_whitespace(it.first, it.second, tabSize), minIndent); } plist::any_t pasteBehaviorValue = bundles::value_for_setting("indentOnPaste", buffer.scope(index)); std::string const* pasteBehavior = boost::get(&pasteBehaviorValue); if(pasteBehavior && *pasteBehavior == "simple") { int currentIndent = indent::leading_whitespace(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size(), tabSize); if(currentIndent) { str = transform::shift(currentIndent-minIndent, buffer.indent())(str); size_t len = str.size(); while(len > 0 && (str[len-1] == '\t' || str[len-1] == ' ')) --len; str = str.substr(0, len); if(!str.empty() && str.back() == '\n') str += leftOfCaret; } } else if(!pasteBehavior || *pasteBehavior != "disable") { indent::fsm_t fsm = indent::create_fsm(buffer, line, indentSize, tabSize); auto const patterns = indent::patterns_for_scope(buffer.scope(index)); for(auto const& it : v) { if(fsm.is_ignored(std::string(it.first, it.second), patterns)) continue; size_t indent = fsm.scan_line(std::string(it.first, it.second), patterns); int oldIndent = indent::leading_whitespace(it.first, it.second, tabSize); transform::shift shifter(std::max(((int)indent)-oldIndent, -minIndent), buffer.indent()); str = shifter(str); break; } if(!str.empty() && str.back() == '\n') str += leftOfCaret; } return ng::move(buffer, replace_helper(buffer, snippets, map(buffer, range_t(buffer.begin(line), selections.last().max()), transform::replace(str))), kSelectionMoveToEndOfSelection); } } return ng::move(buffer, replace_helper(buffer, snippets, map(buffer, selections, transform::replace(str))), kSelectionMoveToEndOfSelection); } void editor_t::insert (std::string const& str, bool selectInsertion) { ng::ranges_t res = replace_helper(_buffer, _snippets, map(_buffer, _selections, transform::replace(str))); _selections = selectInsertion ? res : ng::move(_buffer, res, kSelectionMoveToEndOfSelection); } struct indent_helper_t : ng::callback_t { indent_helper_t (editor_t& editor, buffer_t& buffer, indent_correction_t indentCorrections) : _disabled(indentCorrections == kIndentCorrectNever), _indent_correct(indentCorrections), _editor(editor), _buffer(buffer) { _disabled = _disabled || editor._selections.size() != 1 || editor._selections.last().columnar; if(_disabled) return; _buffer.add_callback(this); } void will_replace (size_t from, size_t to, std::string const& str) { text::pos_t pos = _buffer.convert(from); std::string const line = _buffer.substr(_buffer.begin(pos.line), _buffer.eol(pos.line)); if(_indent_correct == kIndentCorrectNonEmptyLines && (line.empty() || text::is_blank(line.data(), line.data() + line.size()))) return; indent::fsm_t fsm = indent::create_fsm(_buffer, pos.line, _buffer.indent().indent_size(), _buffer.indent().tab_size()); bool ignored = fsm.is_ignored(line, indent::patterns_for_line(_buffer, pos.line)); int actual = indent::leading_whitespace(line.data(), line.data() + line.size(), _buffer.indent().tab_size()); size_t desired = fsm.scan_line(line, indent::patterns_for_line(_buffer, pos.line)); if(ignored || actual == desired) _lines.emplace(pos.line, actual); } ~indent_helper_t () { if(_disabled) return; _buffer.remove_callback(this); std::multimap replacements; for(auto const& pair : _lines) { size_t n = pair.first; size_t bol = _buffer.begin(n); size_t eos = bol; std::string const line = _buffer.substr(bol, _buffer.eol(n)); int actual = indent::leading_whitespace(line.data(), line.data() + line.size(), _buffer.indent().tab_size()); if(actual != pair.second) continue; indent::fsm_t fsm = indent::create_fsm(_buffer, n, _buffer.indent().indent_size(), _buffer.indent().tab_size()); auto const patterns = indent::patterns_for_line(_buffer, n); size_t desired = fsm.scan_line(line, patterns); if(!fsm.is_ignored(line, patterns) && desired != actual) { while(eos != _buffer.size() && text::is_whitespace(_buffer[eos])) eos += _buffer[eos].size(); replacements.emplace(range_t(bol, eos), indent::create(desired, _buffer.indent().tab_size(), _buffer.indent().soft_tabs())); } } if(!replacements.empty()) { preserve_selection_helper_t helper(_buffer, _editor._selections); _editor.replace(replacements); _editor._selections = helper.get(); } } private: bool _disabled; indent_correction_t _indent_correct; editor_t& _editor; buffer_t& _buffer; std::map _lines; }; static std::string find_paired (std::string const& str, scope::context_t const& scope) { plist::any_t typingPairs = bundles::value_for_setting("smartTypingPairs", scope); if(plist::array_t const* typingPairsArray = boost::get(&typingPairs)) { for(auto const& pair : *typingPairsArray) { if(plist::array_t const* pairArray = boost::get(&pair)) { if(pairArray->size() == 2) { std::string const* left = boost::get(&(*pairArray)[0]); std::string const* right = boost::get(&(*pairArray)[1]); if(left && *left == str && right) return *right; } } } } return NULL_STR; } void editor_t::insert_with_pairing (std::string const& str, indent_correction_t indentCorrections, bool autoPairing, std::string const& scopeAttributes) { if(autoPairing && !has_selection()) { size_t const caret = _selections.last().last.index; if(_buffer.pairs().is_last(caret) && caret + str.size() <= _buffer.size() && str == _buffer.substr(caret, caret + str.size())) { _selections = ng::move(_buffer, _selections, kSelectionMoveRight); _buffer.pairs().remove(caret); return; } } indent_helper_t indent_helper(*this, _buffer, indentCorrections); std::string const autoInsert = autoPairing ? find_paired(str, scope(scopeAttributes)) : NULL_STR; if(autoInsert != NULL_STR && has_selection()) { _selections = replace_helper(_buffer, _snippets, map(_buffer, _selections, transform::surround(str, autoInsert))); } else if(autoInsert == NULL_STR || has_selection() || _selections.size() != 1 || _selections.last().columnar || (_selections.last().min().index < _buffer.size() && text::is_word_char(_buffer[_selections.last().min().index]))) { insert(str); } else { if(str == autoInsert && str.size() == 1) { size_t const lineNo = _buffer.convert(_selections.last().last.index).line; std::string line = _buffer.substr(_buffer.begin(lineNo), _buffer.eol(lineNo)); if(std::count(line.begin(), line.end(), str[0]) % 2 == 1) return insert(str); } insert(str + autoInsert); _selections = ng::move(_buffer, _selections, kSelectionMoveLeft); size_t const caret = _selections.last().last.index; _buffer.pairs().add_pair(caret - str.size(), caret); } } void editor_t::move_selection_to (ng::index_t const& index, bool selectInsertion) { std::vector v; std::multimap insertions; for(auto const& range : dissect_columnar(_buffer, _selections)) { v.push_back(_buffer.substr(range.min().index, range.max().index)); insertions.emplace(range, ""); } insertions.emplace(index, text::join(v, "\n")); ranges_t sel, tmp = this->replace(insertions, selectInsertion); for(auto const& range : tmp) { if(!range.empty()) sel.push_back(range); } _selections = tmp.empty() ? tmp : sel; } void editor_t::snippet (std::string const& str, std::map const& variables, bool disableIndent) { size_t from = _selections.last().min().index; size_t to = _selections.last().max().index; _selections = this->snippet(from, to, str, variables, disableIndent); } void editor_t::perform (action_t action, layout_t const* layout, indent_correction_t indentCorrections, std::string const& scopeAttributes) { static std::string const kSingleMarkType = "•"; preserve_selection_helper_t selectionHelper(_buffer, _selections); if(action == kDeleteBackward && !has_selection() && _selections.size() == 1) { size_t caret = _selections.last().last.index; size_t from = caret - (0 < caret ? _buffer[caret-1].size() : 0); if(caret && _buffer.pairs().is_first(from)) { size_t other = _buffer.pairs().counterpart(from); size_t to = other + _buffer[other].size(); if(other == caret) { _selections = range_t(from, to); action = kDeleteSelection; } else { std::multimap insertions; insertions.emplace(range_t(from, caret), ""); insertions.emplace(range_t(other, to), ""); _selections = this->replace(insertions).first(); action = kNop; } } } switch(action) { case kDeleteBackward: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendLeft, layout); break; case kDeleteBackwardByDecomposingPreviousCharacter: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendLeft, layout); break; case kDeleteForward: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendRight, layout); break; case kDeleteSubWordLeft: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToBeginOfSubWord, layout); break; case kDeleteSubWordRight: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfSubWord, layout); break; case kDeleteWordBackward: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToBeginOfWord, layout); break; case kDeleteWordForward: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfWord, layout); break; case kDeleteToBeginningOfIndentedLine: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToBeginOfIndentedLine, layout); break; case kDeleteToEndOfIndentedLine: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfIndentedLine, layout); break; case kDeleteToBeginningOfLine: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToBeginOfSoftLine, layout); break; case kDeleteToEndOfLine: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfSoftLine, layout); break; case kDeleteToBeginningOfParagraph: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToBeginOfParagraph, layout); break; case kDeleteToEndOfParagraph: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfParagraph, layout); break; case kChangeCaseOfWord: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToEndOfWord, layout); break; case kChangeCaseOfLetter: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendRight, layout); break; case kUppercaseWord: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToWord, layout); break; case kLowercaseWord: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToWord, layout); break; case kCapitalizeWord: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToLineExclLF, layout); break; case kReformatText: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToParagraph, layout); break; case kReformatTextAndJustify: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToParagraph, layout); break; case kUnwrapText: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToParagraph, layout); break; case kMoveSelectionUp: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToLine, layout); break; case kMoveSelectionDown: _selections = ng::extend_if_empty(_buffer, _selections, kSelectionExtendToLine, layout); break; case kShiftLeft: _selections = ng::extend(_buffer, _selections, kSelectionExtendToLineExclLF, layout); break; case kShiftRight: _selections = ng::extend(_buffer, _selections, kSelectionExtendToLineExclLF, layout); break; } static std::set const deleteActions = { kDeleteBackward, kDeleteForward }; static std::set const yankAppendActions = { kDeleteSubWordRight, kDeleteWordForward, kDeleteToEndOfIndentedLine, kDeleteToEndOfLine, kDeleteToEndOfParagraph }; static std::set const yankPrependActions = { kDeleteSubWordLeft, kDeleteWordBackward, kDeleteToBeginningOfIndentedLine, kDeleteToBeginningOfLine, kDeleteToBeginningOfParagraph }; if(deleteActions.find(action) != deleteActions.end()) action = kDeleteSelection; else if(yankAppendActions.find(action) != yankAppendActions.end()) action = _extend_yank_clipboard ? kAppendSelectionToYankPboard : kCopySelectionToYankPboard; else if(yankPrependActions.find(action) != yankPrependActions.end()) action = _extend_yank_clipboard ? kPrependSelectionToYankPboard : kCopySelectionToYankPboard; _extend_yank_clipboard = false; switch(action) { case kMoveBackward: _selections = ng::move(_buffer, _selections, kSelectionMoveLeft, layout); break; case kMoveForward: _selections = ng::move(_buffer, _selections, kSelectionMoveRight, layout); break; case kMoveUp: _selections = ng::move(_buffer, _selections, kSelectionMoveUp, layout); break; case kMoveDown: _selections = ng::move(_buffer, _selections, kSelectionMoveDown, layout); break; case kMoveSubWordLeft: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfSubWord, layout); break; case kMoveSubWordRight: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfSubWord, layout); break; case kMoveWordBackward: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfWord, layout); break; case kMoveWordForward: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfWord, layout); break; case kMoveToBeginningOfIndentedLine: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfIndentedLine, layout); break; case kMoveToEndOfIndentedLine: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfIndentedLine, layout); break; case kMoveToBeginningOfLine: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfSoftLine, layout); break; case kMoveToEndOfLine: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfSoftLine, layout); break; case kMoveToBeginningOfParagraph: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfLine, layout); break; case kMoveToEndOfParagraph: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfLine, layout); break; case kMoveToBeginningOfBlock: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfTypingPair, layout); break; case kMoveToEndOfBlock: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfTypingPair, layout); break; case kMoveToBeginningOfColumn: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfColumn, layout); break; case kMoveToEndOfColumn: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfColumn, layout); break; case kMoveToBeginningOfDocument: _selections = ng::move(_buffer, _selections, kSelectionMoveToBeginOfDocument, layout); break; case kMoveToEndOfDocument: _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfDocument, layout); break; case kPageUp: _selections = ng::move(_buffer, _selections, kSelectionMovePageUp, layout); break; case kPageDown: _selections = ng::move(_buffer, _selections, kSelectionMovePageDown, layout); break; case kMoveBackwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendLeft, layout); break; case kMoveForwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendRight, layout); break; case kMoveUpAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendUp, layout); break; case kMoveDownAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendDown, layout); break; case kMoveSubWordLeftAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfSubWord, layout); break; case kMoveSubWordRightAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfSubWord, layout); break; case kMoveWordBackwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfWord, layout); break; case kMoveWordForwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfWord, layout); break; case kMoveToBeginningOfIndentedLineAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfIndentedLine, layout); break; case kMoveToEndOfIndentedLineAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfIndentedLine, layout); break; case kMoveToBeginningOfLineAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfSoftLine, layout); break; case kMoveToEndOfLineAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfSoftLine, layout); break; case kMoveToBeginningOfParagraphAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfParagraph, layout); break; case kMoveToEndOfParagraphAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfParagraph, layout); break; case kMoveParagraphBackwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfLine, layout); break; case kMoveParagraphForwardAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfLine, layout); break; case kMoveToBeginningOfBlockAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfTypingPair, layout); break; case kMoveToEndOfBlockAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfTypingPair, layout); break; case kMoveToBeginningOfColumnAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfColumn, layout); break; case kMoveToEndOfColumnAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfColumn, layout); break; case kMoveToBeginningOfDocumentAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToBeginOfDocument, layout); break; case kMoveToEndOfDocumentAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendToEndOfDocument, layout); break; case kPageUpAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendPageUp, layout); break; case kPageDownAndModifySelection: _selections = ng::extend(_buffer, _selections, kSelectionExtendPageDown, layout); break; case kSelectAll: _selections = ng::extend(_buffer, _selections, kSelectionExtendToAll, layout); break; case kSelectCurrentScope: _selections = ng::extend(_buffer, _selections, kSelectionExtendToScope, layout); break; case kSelectBlock: _selections = ng::extend(_buffer, _selections, kSelectionExtendToTypingPair, layout); break; case kSelectHardLine: _selections = ng::extend(_buffer, _selections, kSelectionExtendToLine, layout); break; case kSelectLine: _selections = ng::extend(_buffer, _selections, kSelectionExtendToSoftLine, layout); break; case kSelectParagraph: _selections = ng::extend(_buffer, _selections, kSelectionExtendToParagraph, layout); break; case kSelectWord: _selections = ng::extend(_buffer, _selections, kSelectionExtendToWord, layout); break; case kToggleColumnSelection: _selections = ng::toggle_columnar(_selections); break; case kFindNext: case kFindPrevious: case kFindNextAndModifySelection: case kFindPreviousAndModifySelection: case kFindAll: case kFindAllInSelection: { if(clipboard_t::entry_ptr findEntry = find_clipboard()->current()) { find::options_t options = convert(findEntry->options()); if(action == kFindNextAndModifySelection || action == kFindPreviousAndModifySelection) options = options | find::extend_selection; if(action == kFindPrevious || action == kFindPreviousAndModifySelection) options = options | find::backwards; else if(action == kFindAll || action == kFindAllInSelection) options = options | find::all_matches; find(findEntry->content(), options, (action == kFindAll || action == kFindAllInSelection) && has_selection()); } } break; case kReplaceAll: case kReplaceAllInSelection: { clipboard_t::entry_ptr findEntry = find_clipboard()->current(); clipboard_t::entry_ptr replaceEntry = replace_clipboard()->current(); if(findEntry && replaceEntry) { find::options_t options = convert(findEntry->options()); if(action == kReplaceAll || action == kReplaceAllInSelection) options = options | find::all_matches; replace_all(findEntry->content(), replaceEntry->content(), options, action == kReplaceAllInSelection); } } break; case kReplace: case kReplaceAndFind: { if(clipboard_t::entry_ptr replaceEntry = replace_clipboard()->current()) { std::string replacement = replaceEntry->content(); if(!match_captures().empty()) replacement = format_string::expand(replacement, match_captures()); insert(replacement, true); } if(action == kReplaceAndFind) perform(kFindNext, layout, indentCorrections, scopeAttributes); } break; case kCopySelectionToYankPboard: case kAppendSelectionToYankPboard: case kPrependSelectionToYankPboard: { clipboard_t::entry_ptr entry = copy(_buffer, _selections); if(action != kCopySelectionToYankPboard && !yank_clipboard()->empty()) { if(clipboard_t::entry_ptr oldEntry = yank_clipboard()->current()) { if(action == kAppendSelectionToYankPboard) entry = std::make_shared(oldEntry->content() + entry->content(), "", false, 1, false); else if(action == kPrependSelectionToYankPboard) entry = std::make_shared(entry->content() + oldEntry->content(), "", false, 1, false); } } yank_clipboard()->push_back(entry); _extend_yank_clipboard = true; } // continue case kDeleteSelection: { indent_helper_t indent_helper(*this, _buffer, indentCorrections); _selections = apply(_buffer, _selections, _snippets, &transform::null); } break; case kDeleteBackwardByDecomposingPreviousCharacter: _selections = apply(_buffer, _selections, _snippets, &transform::decompose); break; case kSetMark: { _buffer.remove_all_marks(kSingleMarkType); _buffer.set_mark(_selections.last().last.index, kSingleMarkType); } break; case kDeleteToMark: { std::map const& marks = _buffer.get_marks(0, _buffer.size(), kSingleMarkType); if(marks.size() == 1) { std::multimap replacements; replacements.emplace(range_t(_selections.last().last, marks.begin()->first), ""); _selections = this->replace(replacements); } } break; case kSelectToMark: { std::map const& marks = _buffer.get_marks(0, _buffer.size(), kSingleMarkType); if(marks.size() == 1) _selections = ng::range_t(_selections.last().last, marks.begin()->first); } break; case kSwapWithMark: { std::map marks = _buffer.get_marks(0, _buffer.size(), kSingleMarkType); if(marks.size() == 1) { _buffer.remove_all_marks(kSingleMarkType); _buffer.set_mark(_selections.last().last.index, kSingleMarkType); _selections = ng::index_t(marks.begin()->first); } } break; case kCut: clipboard()->push_back(copy(_buffer, _selections)); _selections = apply(_buffer, _selections, _snippets, &transform::null); break; case kCopy: clipboard()->push_back(copy(_buffer, _selections)); break; case kCopySelectionToFindPboard: find_clipboard()->push_back(copy(_buffer, dissect_columnar(_buffer, _selections).first())); break; case kCopySelectionToReplacePboard: replace_clipboard()->push_back(copy(_buffer, dissect_columnar(_buffer, _selections).first())); break; case kPaste: _selections = paste(_buffer, _selections, _snippets, clipboard()->current()); break; case kPastePrevious: _selections = paste(_buffer, _selections, _snippets, clipboard()->previous()); break; case kPasteNext: _selections = paste(_buffer, _selections, _snippets, clipboard()->next()); break; case kPasteWithoutReindent: insert(clipboard()->current()->content()); break; case kYank: { if(clipboard_t::entry_ptr entry = yank_clipboard()->current()) insert(entry->content()); } break; case kInsertTab: { _snippets.drop_for_pos(_selections.last().last.index); if(_snippets.next()) _selections = _snippets.current(); else _selections = insert_tab_with_indent(_buffer, _selections, _snippets); } break; case kInsertBacktab: { _snippets.drop_for_pos(_selections.last().last.index); if(_snippets.previous()) _selections = _snippets.current(); } break; case kInsertTabIgnoringFieldEditor: insert("\t"); break; case kInsertNewline: _selections = insert_newline_with_indent(_buffer, _selections, _snippets); break; case kInsertNewlineIgnoringFieldEditor: insert("\n"); break; case kTranspose: { std::multimap replacements; auto inserter = std::inserter(replacements, replacements.end()); if(_selections.size() > 1 && not_empty(_buffer, _selections)) { std::multiset ranges(_selections.begin(), _selections.end()); std::vector strings; std::transform(ranges.begin(), ranges.end(), back_inserter(strings), [this](range_t const& r){ return _buffer.substr(r.min().index, r.max().index); }); std::next_permutation(strings.begin(), strings.end()); std::transform(ranges.begin(), ranges.end(), strings.begin(), inserter, [](range_t const& r, std::string const& str){ return std::make_pair(r, str); }); } else { transpose_selections(_buffer, _selections, inserter); } _selections = this->replace(replacements, not_empty(_buffer, _selections)); } break; case kTransposeWords: break; case kIndent: { range_t r = _selections.last(); text::pos_t p0 = _buffer.convert(r.first.index); text::pos_t p1 = _buffer.convert(r.last.index); if(p1 < p0) std::swap(p0, p1); size_t from = p0.line; size_t to = p1.line + (p1.column != 0 || p0.line == p1.line ? 1 : 0); indent::fsm_t fsm = indent::create_fsm(_buffer, from, _buffer.indent().indent_size(), _buffer.indent().tab_size()); std::multimap replacements; for(size_t n = from; n < to; ++n) { size_t bol = _buffer.begin(n); size_t eos = bol; std::string const line = _buffer.substr(bol, _buffer.eol(n)); if(text::is_blank(line.data(), line.data() + line.size())) continue; while(eos != _buffer.size() && text::is_whitespace(_buffer[eos])) eos += _buffer[eos].size(); replacements.emplace(range_t(bol, eos), indent::create(fsm.scan_line(line, indent::patterns_for_line(_buffer, n)), _buffer.indent().tab_size(), _buffer.indent().soft_tabs())); } if(!replacements.empty()) _selections = this->replace(replacements); } break; case kShiftLeft: _selections = apply(_buffer, _selections, _snippets, transform::shift(-_buffer.indent().indent_size(), _buffer.indent())); break; case kShiftRight: _selections = apply(_buffer, _selections, _snippets, transform::shift(+_buffer.indent().indent_size(), _buffer.indent())); break; case kChangeCaseOfWord: _selections = apply(_buffer, _selections, _snippets, &text::opposite_case); break; case kChangeCaseOfLetter: _selections = apply(_buffer, _selections, _snippets, &text::opposite_case); break; case kUppercaseWord: _selections = apply(_buffer, _selections, _snippets, &text::uppercase); break; case kLowercaseWord: _selections = apply(_buffer, _selections, _snippets, &text::lowercase); break; case kCapitalizeWord: _selections = apply(_buffer, _selections, _snippets, &transform::capitalize); break; case kReformatText: case kReformatTextAndJustify: { size_t wrapColumn = layout ? layout->effective_wrap_column() : 80; if(_selections.last().columnar) { text::pos_t const& fromPos = _buffer.convert(_selections.last().min().index); text::pos_t const& toPos = _buffer.convert(_selections.last().max().index); size_t fromCol = visual_distance(_buffer, _buffer.begin(fromPos.line), _selections.last().min()); size_t toCol = visual_distance(_buffer, _buffer.begin(toPos.line), _selections.last().max()); if(toCol < fromCol) std::swap(fromCol, toCol); else if(fromCol == toCol && toCol != 0) fromCol = 0; else if(fromCol == toCol) toCol = std::max(fromCol + 10, wrapColumn); size_t const from = _buffer.begin(fromPos.line); size_t const to = _buffer.eol(toPos.line); std::string str = _buffer.substr(from, to); str = text::trim(str); str = transform::unwrap(str); if(action == kReformatTextAndJustify) str = transform::justify(toCol - fromCol, _buffer.indent().tab_size(), false)(str); else str = transform::reformat(toCol - fromCol, _buffer.indent().tab_size(), false)(str); str = transform::shift(fromCol, text::indent_t(8, 8, true))(str); std::multimap replacements; replacements.emplace(range_t(from, to), str); ng::range_t range = this->replace(replacements, true).first(); _selections = range_t(visual_advance(_buffer, _buffer.begin(_buffer.convert(range.min().index).line), fromCol), visual_advance(_buffer, _buffer.begin(_buffer.convert(range.max().index).line), toCol), true); action = kNop; // skip selection preserving } else { if(action == kReformatTextAndJustify) _selections = apply(_buffer, _selections, _snippets, transform::justify(wrapColumn, _buffer.indent().tab_size())); else _selections = apply(_buffer, _selections, _snippets, transform::reformat(wrapColumn, _buffer.indent().tab_size())); } } break; case kUnwrapText: _selections = apply(_buffer, _selections, _snippets, &transform::unwrap); break; case kComplete: case kNextCompletion: next_completion(scopeAttributes); break; case kPreviousCompletion: previous_completion(scopeAttributes); break; case kMoveSelectionUp: move_selection( 0, -1); break; case kMoveSelectionDown: move_selection( 0, +1); break; case kMoveSelectionLeft: move_selection(-1, 0); break; case kMoveSelectionRight: move_selection(+1, 0); break; } static std::set const preserveSelectionActions = { kCapitalizeWord, kUppercaseWord, kLowercaseWord, kIndent, kShiftLeft, kShiftRight, kReformatText, kReformatTextAndJustify, kUnwrapText }; if(preserveSelectionActions.find(action) != preserveSelectionActions.end()) _selections = selectionHelper.get(); } void editor_t::perform_replacements (std::multimap, std::string> const& replacements) { std::multimap tmp; for(auto pair : replacements) { // D(DBF_Editor, bug("replace range %zu-%zu with ā€˜%s’\n", pair->first.first, pair->first.second, pair->second.c_str());); tmp.emplace(range_t(pair.first.first, pair.first.second), pair.second); } if(!tmp.empty()) { preserve_selection_helper_t helper(_buffer, _selections); this->replace(tmp); _selections = helper.get(); } } ng::ranges_t editor_t::insert_tab_with_indent (ng::buffer_t& buffer, ng::ranges_t const& selections, snippet_controller_t& snippets) { bool smartTabEnabled = selections.size() == 1 && !selections.last().columnar; std::string estimatedIndent = NULL_STR; std::multimap insertions; for(auto const& range : dissect_columnar(buffer, selections)) { size_t const from = range.min().index; size_t const firstLine = buffer.convert(from).line; std::string const& leftOfCaret = buffer.substr(buffer.begin(firstLine), from); if(smartTabEnabled && text::is_blank(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size())) { size_t to = range.max().index; if(estimatedIndent == NULL_STR) { size_t const lastLine = buffer.convert(to).line; std::string const& rightOfCaret = buffer.substr(to, buffer.end(lastLine)); indent::fsm_t fsm = indent::create_fsm(buffer, firstLine, buffer.indent().indent_size(), buffer.indent().tab_size()); std::string const& line = leftOfCaret + rightOfCaret; size_t existingIndent = indent::leading_whitespace(line.data(), line.data() + line.size(), buffer.indent().tab_size()); size_t newIndent = std::max(fsm.scan_line(line, indent::patterns_for_line(buffer, firstLine)), existingIndent + buffer.indent().indent_size()); estimatedIndent = indent::create(newIndent - indent::leading_whitespace(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size(), buffer.indent().tab_size()), buffer.indent().tab_size(), buffer.indent().soft_tabs()); } while(to != buffer.size() && text::is_whitespace(buffer[to])) to += buffer[to].size(); insertions.emplace(ng::range_t(from, to), estimatedIndent); } else { size_t col = visual_distance(buffer, buffer.begin(firstLine), from); insertions.emplace(range, buffer.indent().create(col)); } } return ng::move(buffer, replace_helper(buffer, snippets, insertions), kSelectionMoveToEndOfSelection); } ng::ranges_t editor_t::insert_newline_with_indent (ng::buffer_t& buffer, ng::ranges_t const& selections, snippet_controller_t& snippets) { std::multimap insertions; for(auto const& range : dissect_columnar(buffer, selections)) { size_t const from = range.min().index; size_t const firstLine = buffer.convert(from).line; std::string const& leftOfCaret = buffer.substr(buffer.begin(firstLine), from); auto const patterns = indent::patterns_for_line(buffer, firstLine); size_t to = range.max().index; size_t const lastLine = buffer.convert(to).line; std::string const& rightOfCaret = buffer.substr(to, buffer.end(lastLine)); indent::fsm_t fsm(buffer.indent().indent_size(), buffer.indent().tab_size()); if(!fsm.is_seeded(leftOfCaret, patterns)) { size_t n = firstLine; while(n-- > 0 && !fsm.is_seeded(indent::line(buffer, n), indent::patterns_for_line(buffer, n))) continue; } size_t existingIndent = indent::leading_whitespace(rightOfCaret.data(), rightOfCaret.data() + rightOfCaret.size(), buffer.indent().tab_size()); size_t newIndent = fsm.scan_line(rightOfCaret, patterns); if(text::is_blank(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size())) newIndent = indent::leading_whitespace(leftOfCaret.data(), leftOfCaret.data() + leftOfCaret.size(), buffer.indent().tab_size()) + existingIndent; else existingIndent = 0; insertions.emplace(range, existingIndent < newIndent ? "\n" + indent::create(newIndent - existingIndent, buffer.indent().tab_size(), buffer.indent().soft_tabs()) : "\n"); } return ng::move(buffer, replace_helper(buffer, snippets, insertions), kSelectionMoveToEndOfSelection); } void editor_t::move_selection (int deltaX, int deltaY) { ssize_t adjustment = 0; std::map clips; for(auto const& range : dissect_columnar(_buffer, _selections).sorted()) { range_t r = range + adjustment; std::string const& str = _buffer.substr(r.min().index, r.max().index); std::multimap replacements; replacements.emplace(r, ""); ranges_t res = this->replace(replacements); clips.emplace(res.first().first.index, str); adjustment -= str.size(); } std::multimap replacements; for(auto const& pair : clips) { text::pos_t pos = _buffer.convert(pair.first); int line = pos.line; int col = visual_distance(_buffer, _buffer.begin(line), pair.first, false); line = oak::cap(0, line + deltaY, int(_buffer.lines()-1)); col = std::max(col + deltaX, 0); replacements.emplace(visual_advance(_buffer, _buffer.begin(line), col, false), pair.second); } _selections = this->replace(replacements, true); } bool editor_t::handle_result (std::string const& uncheckedOut, output::type placement, output_format::type format, output_caret::type outputCaret, ng::range_t input_range, std::map environment) { std::string const& out = utf8::is_valid(uncheckedOut.begin(), uncheckedOut.end()) ? uncheckedOut : sanitized_utf8(uncheckedOut); if(input_range.columnar) { text::pos_t const fromPos = _buffer.convert(input_range.min().index); text::pos_t const toPos = _buffer.convert(input_range.max().index); size_t fromCol = visual_distance(_buffer, _buffer.begin(fromPos.line), input_range.min()); size_t toCol = visual_distance(_buffer, _buffer.begin(toPos.line), input_range.max()); if(toCol < fromCol) { std::swap(fromCol, toCol); input_range.first = visual_advance(_buffer, _buffer.begin(fromPos.line), fromCol); input_range.last = visual_advance(_buffer, _buffer.begin(toPos.line), toCol); } } range_t range; switch(placement) { case output::replace_input: range = input_range; break; case output::replace_document: range = range_t(0, _buffer.size()); break; case output::at_caret: range = _selections.last().last; break; case output::after_input: range = input_range.max(); break; case output::replace_selection: range = _selections.last(); break; } size_t caret = _selections.last().last.index; size_t line = 0, column = 0, offset = 0; if(range && format == output_format::text && outputCaret == output_caret::interpolate_by_char) { ASSERT_LE(range.min().index, caret); ASSERT_LE(caret, range.max().index); offset = caret - range.min().index; // TODO we should let offset be number of code points (for non-combining marks) } else if(format == output_format::text && outputCaret == output_caret::interpolate_by_line) { line = _buffer.convert(caret).line; column = visual_distance(_buffer, _buffer.begin(line), caret); } switch(format) { case output_format::snippet: case output_format::snippet_no_auto_indent: { if(range) _selections = range; snippet(out, environment, format == output_format::snippet_no_auto_indent); } break; case output_format::text: { size_t bol = 0, eol = out.find('\n'); if(placement == output::replace_input && range.columnar && eol != std::string::npos) { std::multimap replacements; for(auto const& r : dissect_columnar(_buffer, range)) { replacements.emplace(r, out.substr(bol == std::string::npos ? out.size() : bol, eol == std::string::npos ? eol : eol - bol)); bol = eol == std::string::npos ? eol : eol + 1; eol = bol == std::string::npos ? bol : out.find('\n', bol); } _selections = replace_helper(_buffer, _snippets, replacements); if(outputCaret != output_caret::select_output) _selections = ng::move(_buffer, _selections, kSelectionMoveToEndOfSelection); } else { if(range) _selections = range; insert(out, outputCaret == output_caret::select_output); } if(range && outputCaret == output_caret::interpolate_by_char) { offset = utf8::find_safe_end(out.begin(), out.begin() + std::min(offset, out.size())) - out.begin(); _selections = range_t(range.min().index + offset); } else if(outputCaret == output_caret::interpolate_by_line) { _selections = visual_advance(_buffer, _buffer.begin(std::min(line, _buffer.lines()-1)), column); } } break; // case output_format::completion_list: // { // std::vector completions; // for(auto const& line : text::tokenize(out.begin(), out.end(), '\n')) // { // if(!line.empty()) // completions.push_back(line); // } // completion(completions, selection().last()); // } // break; } return true; } void editor_t::delete_tab_trigger (std::string const& str) { ranges_t ranges; for(auto const& range : _selections) { size_t from = range.min().index; ranges.push_back(range_t(from - std::min(str.size(), from), range.max())); } _selections = apply(_buffer, ranges, _snippets, &transform::null); } scope::context_t editor_t::scope (std::string const& scopeAttributes) const { return ng::scope(_buffer, _selections, scopeAttributes); } std::map editor_t::editor_variables (std::string const& scopeAttributes) const { std::map map = { { "TM_TAB_SIZE", std::to_string(_buffer.indent().tab_size()) }, { "TM_SOFT_TABS", _buffer.indent().soft_tabs() ? "YES" : "NO" }, { "TM_SELECTION", to_s(_buffer, _selections) }, }; scope::context_t const& s = scope(scopeAttributes); map.emplace("TM_SCOPE", to_s(s.right)); if(_selections.size() == 1) { range_t const range = _selections.last(); if(range.empty()) { size_t const caret = range.last.index; text::pos_t const& pos = _buffer.convert(caret); map.emplace("TM_LINE_INDEX", std::to_string(pos.column)); map.emplace("TM_LINE_NUMBER", std::to_string(pos.line+1)); map.emplace("TM_COLUMN_NUMBER", std::to_string(visual_distance(_buffer, _buffer.begin(pos.line), caret)+1)); map.emplace("TM_CURRENT_LINE", _buffer.substr(_buffer.begin(pos.line), _buffer.eol(pos.line))); map.emplace("TM_SCOPE_LEFT", to_s(s.left)); range_t wordRange = ng::word_at(_buffer, range); if(!wordRange.empty()) map.emplace("TM_CURRENT_WORD", _buffer.substr(wordRange.min().index, wordRange.max().index)); } else { if(32 + range.max().index - range.min().index < ARG_MAX) { bool first = true; for(auto const& r : dissect_columnar(_buffer, range)) { if(std::exchange(first, false)) map["TM_SELECTED_TEXT"] = ""; else map["TM_SELECTED_TEXT"] += "\n"; map["TM_SELECTED_TEXT"] += _buffer.substr(r.min().index, r.max().index); } } else { map.emplace("TM_SELECTED_TEXT", text::format("Error: Selection exceeds %s. Command should read selection from stdin.", text::format_size(ARG_MAX-32).c_str())); } } } return map; } // ======== // = Find = // ======== void editor_t::find (std::string const& searchFor, find::options_t options, bool searchOnlySelection) { set_match_captures(std::map()); ranges_t res; if(options & find::all_matches) { for(auto const& pair : ng::find_all(_buffer, searchFor, options, searchOnlySelection ? _selections : ranges_t())) res.push_back(pair.first); if(searchOnlySelection && res.sorted() == _selections.sorted()) { res = ranges_t(); for(auto const& pair : ng::find_all(_buffer, searchFor, options, ranges_t())) res.push_back(pair.first); } } else { auto matches = ng::find(_buffer, _selections, searchFor, options); for(auto const& pair : matches) res.push_back(pair.first); if(matches.size() == 1) set_match_captures(matches.begin()->second); } if(!res.empty()) _selections = res; } ranges_t editor_t::replace_all (std::string const& searchFor, std::string const& replaceWith, find::options_t options, bool searchOnlySelection) { ranges_t res; if(options & find::all_matches) { preserve_selection_helper_t helper(_buffer, _selections); std::multimap replacements; for(auto const& pair : ng::find_all(_buffer, searchFor, options, searchOnlySelection ? _selections : ranges_t())) replacements.emplace(pair.first, options & find::regular_expression ? format_string::expand(replaceWith, pair.second) : replaceWith); res = this->replace(replacements, true); _selections = helper.get(); } return res; } } /* ng */