#include "editor.hh" #include "exception.hh" #include "utils.hh" #include "register.hh" #include "register_manager.hh" #include "utf8_iterator.hh" #include namespace Kakoune { Editor::Editor(Buffer& buffer) : m_buffer(&buffer), m_edition_level(0), m_selections(buffer) { m_selections.push_back(Selection(buffer.begin(), buffer.begin())); } void Editor::erase() { scoped_edition edition(*this); for (auto& sel : m_selections) { m_buffer->erase(sel.begin(), sel.end()); sel.avoid_eol(); } } static BufferIterator prepare_insert(Buffer& buffer, const Selection& sel, InsertMode mode) { switch (mode) { case InsertMode::Insert: case InsertMode::Replace: return sel.begin(); case InsertMode::Append: { // special case for end of lines, append to current line instead auto pos = std::max(sel.first(), sel.last()); if (pos.column() == buffer.line_length(pos.line()) - 1) return pos; else return pos+1; } case InsertMode::InsertAtLineBegin: return buffer.iterator_at_line_begin(sel.begin()); case InsertMode::AppendAtLineEnd: return buffer.iterator_at_line_end(sel.end()-1)-1; case InsertMode::InsertAtNextLineBegin: return buffer.iterator_at_line_end(sel.end()-1); case InsertMode::OpenLineBelow: { LineCount line = (sel.end() - 1).line(); buffer.insert(buffer.iterator_at_line_end(line), "\n"); return buffer.iterator_at_line_begin(line + 1); } case InsertMode::OpenLineAbove: { auto pos = buffer.iterator_at_line_begin(sel.begin()); buffer.insert(pos, "\n"); return pos; } } assert(false); return BufferIterator{}; } void Editor::insert(const String& string, InsertMode mode) { scoped_edition edition(*this); if (mode == InsertMode::Replace) { // do not call Editor::erase as we do not want to avoid end of lines for (auto& sel : m_selections) m_buffer->erase(sel.begin(), sel.end()); } for (auto& sel : m_selections) { BufferIterator pos = prepare_insert(*m_buffer, sel, mode); m_buffer->insert(pos, string); if (mode == InsertMode::Replace) { sel.first() = pos; sel.last() = pos + (string.empty() ? 0 : string.length() - 1 ); } sel.avoid_eol(); } } void Editor::insert(const memoryview& strings, InsertMode mode) { scoped_edition edition(*this); if (mode == InsertMode::Replace) { // do not call Editor::erase as we do not want to avoid end of lines for (auto& sel : m_selections) m_buffer->erase(sel.begin(), sel.end()); } if (strings.empty()) return; for (size_t i = 0; i < selections().size(); ++i) { auto& sel = m_selections[i]; BufferIterator pos = prepare_insert(*m_buffer, sel, mode); const String& str = strings[std::min(i, strings.size()-1)]; m_buffer->insert(pos, str); if (mode == InsertMode::Replace) { sel.first() = pos; sel.last() = pos + (str.empty() ? 0 : str.length() - 1); } sel.avoid_eol(); } } std::vector Editor::selections_content() const { std::vector contents; for (auto& sel : m_selections) contents.push_back(m_buffer->string(sel.begin(), sel.end())); return contents; } static bool compare_selections(const Selection& lhs, const Selection& rhs) { return lhs.begin() < rhs.begin(); } static void sort_and_merge_overlapping(SelectionList& selections) { if (selections.size() == 1) return; Range back = selections.back(); auto back_rank = std::count_if(selections.begin(), selections.end(), [&](const Selection& sel) { return sel.begin() <= back.begin(); }); std::stable_sort(selections.begin(), selections.end(), compare_selections); if (back_rank < selections.size() - 1) std::rotate(selections.begin(), selections.begin() + back_rank, selections.end()); assert(selections.back() == back); for (size_t i = 0; i < selections.size() and selections.size() > 1;) { size_t next = (i + 1) % selections.size(); if (overlaps(selections[i], selections[next])) { selections[i].merge_with(selections[next]); selections.erase(selections.begin() + next); } else ++i; } } void Editor::move_selections(CharCount offset, SelectMode mode) { assert(mode == SelectMode::Replace or mode == SelectMode::Extend); for (auto& sel : m_selections) { auto last = sel.last(); auto limit = offset < 0 ? buffer().iterator_at_line_begin(last) : utf8::previous(buffer().iterator_at_line_end(last)); last = utf8::advance(last, limit, offset); sel.first() = mode == SelectMode::Extend ? sel.first() : last; sel.last() = last; sel.avoid_eol(); } sort_and_merge_overlapping(m_selections); } void Editor::move_selections(LineCount offset, SelectMode mode) { assert(mode == SelectMode::Replace or mode == SelectMode::Extend); for (auto& sel : m_selections) { BufferCoord pos = sel.last().coord(); pos.line += offset; BufferIterator last = utf8::finish(m_buffer->iterator_at(pos, true)); sel.first() = mode == SelectMode::Extend ? sel.first() : last; sel.last() = last; sel.avoid_eol(); } sort_and_merge_overlapping(m_selections); } void Editor::clear_selections() { auto& sel = m_selections.back(); auto& pos = sel.last(); if (*pos == '\n' and not pos.is_begin() and *utf8::previous(pos) != '\n') pos = utf8::previous(pos); sel.first() = pos; m_selections.erase(m_selections.begin(), m_selections.end() - 1); check_invariant(); } void Editor::flip_selections() { for (auto& sel : m_selections) std::swap(sel.first(), sel.last()); check_invariant(); } void Editor::keep_selection(int index) { if (index < m_selections.size()) m_selections = SelectionList{ std::move(m_selections[index]) }; check_invariant(); } void Editor::remove_selection(int index) { if (m_selections.size() > 1 and index < m_selections.size()) m_selections.erase(m_selections.begin() + index); check_invariant(); } void Editor::select(const Selection& selection, SelectMode mode) { if (mode == SelectMode::Replace) m_selections = SelectionList{ selection }; else if (mode == SelectMode::Extend) { for (auto& sel : m_selections) sel.merge_with(selection); sort_and_merge_overlapping(m_selections); } else if (mode == SelectMode::Append) { m_selections.push_back(selection); sort_and_merge_overlapping(m_selections); } else assert(false); check_invariant(); } void Editor::select(SelectionList selections) { if (selections.empty()) throw runtime_error("no selections"); m_selections = std::move(selections); check_invariant(); } void Editor::select(const Selector& selector, SelectMode mode) { if (mode == SelectMode::Append) { auto& sel = m_selections.back(); auto res = selector(sel); if (res.captures().empty()) res.captures() = sel.captures(); m_selections.push_back(res); } else if (mode == SelectMode::ReplaceLast) { auto& sel = m_selections.back(); auto res = selector(sel); sel.first() = res.first(); sel.last() = res.last(); if (not res.captures().empty()) sel.captures() = std::move(res.captures()); } else { for (auto& sel : m_selections) { auto res = selector(sel); if (mode == SelectMode::Extend) sel.merge_with(res); else { sel.first() = res.first(); sel.last() = res.last(); } if (not res.captures().empty()) sel.captures() = std::move(res.captures()); } } sort_and_merge_overlapping(m_selections); check_invariant(); } struct nothing_selected : public runtime_error { nothing_selected() : runtime_error("nothing was selected") {} }; void Editor::multi_select(const MultiSelector& selector) { SelectionList new_selections; for (auto& sel : m_selections) { SelectionList res = selector(sel); for (auto& new_sel : res) { // preserve captures when selectors captures nothing. if (new_sel.captures().empty()) new_selections.emplace_back(new_sel.first(), new_sel.last(), sel.captures()); else new_selections.push_back(std::move(new_sel)); } } if (new_selections.empty()) throw nothing_selected(); sort_and_merge_overlapping(new_selections); m_selections = std::move(new_selections); check_invariant(); } class LastModifiedRangeListener : public BufferChangeListener { public: LastModifiedRangeListener(Buffer& buffer) : m_buffer(buffer) { m_buffer.change_listeners().add(this); } ~LastModifiedRangeListener() { m_buffer.change_listeners().remove(this); } void on_insert(const BufferIterator& begin, const BufferIterator& end) { m_first = begin; m_last = utf8::previous(end); } void on_erase(const BufferIterator& begin, const BufferIterator& end) { m_first = begin; if (m_first >= m_buffer.end()) m_first = utf8::previous(m_buffer.end()); m_last = m_first; } const BufferIterator& first() const { return m_first; } const BufferIterator& last() const { return m_last; } private: BufferIterator m_first; BufferIterator m_last; Buffer& m_buffer; }; bool Editor::undo() { LastModifiedRangeListener listener(buffer()); bool res = m_buffer->undo(); if (res) m_selections = SelectionList{ {listener.first(), listener.last()} }; check_invariant(); return res; } bool Editor::redo() { LastModifiedRangeListener listener(buffer()); bool res = m_buffer->redo(); if (res) m_selections = SelectionList{ {listener.first(), listener.last()} }; check_invariant(); return res; } void Editor::check_invariant() const { assert(not m_selections.empty()); m_selections.check_invariant(); auto it = ++std::max_element(m_selections.begin(), m_selections.end(), compare_selections); assert(std::is_sorted(m_selections.begin(), it, compare_selections)); assert(std::is_sorted(it, m_selections.end(), compare_selections)); } void Editor::begin_edition() { ++m_edition_level; if (m_edition_level == 1) m_buffer->begin_undo_group(); } void Editor::end_edition() { assert(m_edition_level > 0); if (m_edition_level == 1) m_buffer->end_undo_group(); --m_edition_level; } using utf8_it = utf8::utf8_iterator; IncrementalInserter::IncrementalInserter(Editor& editor, InsertMode mode) : m_editor(editor), m_edition(editor), m_mode(mode) { Buffer& buffer = *editor.m_buffer; if (mode == InsertMode::Replace) { for (auto& sel : editor.m_selections) buffer.erase(sel.begin(), sel.end()); } for (auto& sel : m_editor.m_selections) { utf8_it first, last; switch (mode) { case InsertMode::Insert: first = utf8_it(sel.end()) - 1; last = sel.begin(); break; case InsertMode::Replace: first = utf8_it(sel.end()) - 1; last = sel.begin(); break; case InsertMode::Append: { first = sel.begin(); last = std::max(sel.first(), sel.last()); // special case for end of lines, append to current line instead auto coord = last.underlying_iterator().coord(); if (coord.column != buffer.line_length(coord.line) - 1) ++last; break; } case InsertMode::OpenLineBelow: case InsertMode::AppendAtLineEnd: first = utf8_it(buffer.iterator_at_line_end(utf8::previous(sel.end()))) - 1; last = first; break; case InsertMode::OpenLineAbove: case InsertMode::InsertAtLineBegin: first = buffer.iterator_at_line_begin(sel.begin()); if (mode == InsertMode::OpenLineAbove) --first; else { auto first_non_blank = first; while (*first_non_blank == ' ' or *first_non_blank == '\t') ++first_non_blank; if (*first_non_blank != '\n') first = first_non_blank; } last = first; break; } if (first.underlying_iterator().is_end()) --first; if (last.underlying_iterator().is_end()) --last; sel.first() = first.underlying_iterator(); sel.last() = last.underlying_iterator(); } if (mode == InsertMode::OpenLineBelow or mode == InsertMode::OpenLineAbove) { insert("\n"); if (mode == InsertMode::OpenLineAbove) { for (auto& sel : m_editor.m_selections) { // special case, the --first line above did nothing, so we need to compensate now if (sel.first() == utf8::next(buffer.begin())) sel.first() = sel.last() = buffer.begin(); } } } editor.check_invariant(); } IncrementalInserter::~IncrementalInserter() { for (auto& sel : m_editor.m_selections) { if (m_mode == InsertMode::Append and sel.last().column() > 0) sel.last() = utf8::previous(sel.last()); sel.avoid_eol(); } } void IncrementalInserter::insert(String content) { Buffer& buffer = m_editor.buffer(); for (auto& sel : m_editor.m_selections) { m_editor.filters()(buffer, sel, content); buffer.insert(sel.last(), content); } } void IncrementalInserter::insert(const memoryview& strings) { for (size_t i = 0; i < m_editor.m_selections.size(); ++i) { size_t index = std::min(i, strings.size()-1); m_editor.buffer().insert(m_editor.m_selections[i].last(), strings[index]); } } void IncrementalInserter::erase() { for (auto& sel : m_editor.m_selections) { BufferIterator pos = sel.last(); m_editor.buffer().erase(utf8::previous(pos), pos); } } void IncrementalInserter::move_cursors(CharCount move) { m_editor.move_selections(move, SelectMode::Replace); } void IncrementalInserter::move_cursors(LineCount move) { m_editor.move_selections(move, SelectMode::Replace); } }