diff --git a/src/client.cc b/src/client.cc index 3ddddd87..ec90982c 100644 --- a/src/client.cc +++ b/src/client.cc @@ -60,6 +60,9 @@ Client::Client(std::unique_ptr&& ui, else m_pending_keys.push_back(key); }); + m_ui->set_on_paste([this](StringView content) { + context().input_handler().paste(content); + }); m_window->hooks().run_hook(Hook::WinDisplay, m_window->buffer().name(), context()); diff --git a/src/input_handler.cc b/src/input_handler.cc index 69ef3385..877517e2 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -32,6 +32,7 @@ public: InputMode& operator=(const InputMode&) = delete; void handle_key(Key key) { RefPtr keep_alive{this}; on_key(key); } + virtual void paste(StringView content); virtual void on_enabled() {} virtual void on_disabled(bool temporary) {} @@ -71,6 +72,32 @@ private: InputHandler& m_input_handler; }; +void InputMode::paste(StringView content) +{ + try + { + Buffer& buffer = context().buffer(); + const bool linewise = not content.empty() and content.back() == '\n'; + ScopedEdition edition{context()}; + ScopedSelectionEdition selection_edition{context()}; + context().selections().for_each([&buffer, content=std::move(content), linewise] + (size_t index, Selection& sel) { + auto& min = sel.min(); + auto& max = sel.max(); + BufferRange range = + buffer.insert(paste_pos(buffer, min, max, PasteMode::Insert, linewise), content); + min = range.begin; + max = range.end > range.begin ? buffer.char_prev(range.end) : range.begin; + }, false); + } + catch (Kakoune::runtime_error& error) + { + write_to_debug_buffer(format("Error: {}", error.what())); + context().print_status({error.what().str(), context().faces()["Error"] }); + context().hooks().run_hook(Hook::RuntimeError, error.what(), context()); + } +} + namespace InputModes { @@ -1015,6 +1042,17 @@ public: m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context())); } + void paste(StringView content) override + { + m_line_editor.insert(content); + clear_completions(); + m_refresh_completion_pending = true; + display(); + m_line_changed = true; + if (not (context().flags() & Context::Flags::Draft)) + m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context())); + } + void set_prompt_face(Face face) { if (face != m_prompt_face) @@ -1433,6 +1471,12 @@ public: m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context())); } + void paste(StringView content) override + { + insert(ConstArrayView{content}); + m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context())); + } + DisplayLine mode_line() const override { auto num_sel = context().selections().size(); @@ -1462,7 +1506,8 @@ private: selections.sort_and_merge_overlapping(); } - void insert(ConstArrayView strings) + template + void insert(ConstArrayView strings) { m_completer.try_accept(); context().selections().for_each([strings, &buffer=context().buffer()] @@ -1474,7 +1519,7 @@ private: void insert(Codepoint key) { String str{key}; - insert(str); + insert(ConstArrayView{str}); context().hooks().run_hook(Hook::InsertChar, str, context()); } @@ -1644,6 +1689,11 @@ void InputHandler::repeat_last_insert() kak_assert(dynamic_cast(¤t_mode()) != nullptr); } +void InputHandler::paste(StringView content) +{ + current_mode().paste(content); +} + void InputHandler::prompt(StringView prompt, String initstr, String emptystr, Face prompt_face, PromptFlags flags, char history_register, PromptCompleter completer, PromptCallback callback) diff --git a/src/input_handler.hh b/src/input_handler.hh index 5cb150b5..b7e53aaf 100644 --- a/src/input_handler.hh +++ b/src/input_handler.hh @@ -72,6 +72,8 @@ public: void insert(InsertMode mode, int count); // repeat last insert mode key sequence void repeat_last_insert(); + // insert a string without affecting the mode stack + void paste(StringView content); // enter prompt mode, callback is called on each change, // abort or validation with corresponding PromptEvent value diff --git a/src/json_ui.cc b/src/json_ui.cc index 8f0af46d..1200ed60 100644 --- a/src/json_ui.cc +++ b/src/json_ui.cc @@ -211,6 +211,11 @@ void JsonUI::set_on_key(OnKeyCallback callback) m_on_key = std::move(callback); } +void JsonUI::set_on_paste(OnPasteCallback callback) +{ + m_on_paste = std::move(callback); +} + void JsonUI::eval_json(const Value& json) { if (not json.is_a()) diff --git a/src/json_ui.hh b/src/json_ui.hh index 7b1abf44..545e4f35 100644 --- a/src/json_ui.hh +++ b/src/json_ui.hh @@ -46,6 +46,7 @@ public: DisplayCoord dimensions() override; void set_on_key(OnKeyCallback callback) override; + void set_on_paste(OnPasteCallback callback) override; void set_ui_options(const Options& options) override; private: @@ -54,6 +55,7 @@ private: FDWatcher m_stdin_watcher; OnKeyCallback m_on_key; + OnPasteCallback m_on_paste; Vector m_pending_keys; DisplayCoord m_dimensions; String m_requests; diff --git a/src/main.cc b/src/main.cc index 8ce47d8e..fef83090 100644 --- a/src/main.cc +++ b/src/main.cc @@ -626,6 +626,7 @@ std::unique_ptr make_ui(UIType ui_type) void set_cursor(CursorMode, DisplayCoord) override {} void refresh(bool) override {} void set_on_key(OnKeyCallback) override {} + void set_on_paste(OnPasteCallback) override {} void set_ui_options(const Options&) override {} }; diff --git a/src/normal.cc b/src/normal.cc index 7dc14382..e3fdee16 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -657,13 +657,6 @@ void change(Context& context, NormalParams params) enter_insert_mode(context, params); } -enum class PasteMode -{ - Append, - Insert, - Replace -}; - BufferCoord paste_pos(Buffer& buffer, BufferCoord min, BufferCoord max, PasteMode mode, bool linewise) { switch (mode) diff --git a/src/normal.hh b/src/normal.hh index ad0f81be..080c3d01 100644 --- a/src/normal.hh +++ b/src/normal.hh @@ -10,6 +10,7 @@ namespace Kakoune { +class Buffer; class Context; struct no_selections_remaining : runtime_error @@ -40,6 +41,15 @@ struct KeyInfo String build_autoinfo_for_mapping(const Context& context, KeymapMode mode, ConstArrayView built_ins); +enum class PasteMode +{ + Append, + Insert, + Replace +}; + +BufferCoord paste_pos(Buffer& buffer, BufferCoord min, BufferCoord max, PasteMode mode, bool linewise); + } #endif // normal_hh_INCLUDED diff --git a/src/remote.cc b/src/remote.cc index 50dda1b0..5b97f985 100644 --- a/src/remote.cc +++ b/src/remote.cc @@ -43,6 +43,7 @@ enum class MessageType : uint8_t SetOptions, Exit, Key, + Paste, }; class MsgWriter @@ -413,6 +414,9 @@ public: void set_on_key(OnKeyCallback callback) override { m_on_key = std::move(callback); } + void set_on_paste(OnPasteCallback callback) override + { m_on_paste = std::move(callback); } + void set_ui_options(const Options& options) override; void exit(int status); @@ -430,6 +434,7 @@ private: MsgReader m_reader; DisplayCoord m_dimensions; OnKeyCallback m_on_key; + OnPasteCallback m_on_paste; RemoteBuffer m_send_buffer; }; @@ -479,17 +484,25 @@ RemoteUI::RemoteUI(int socket, DisplayCoord dimensions) if (not m_reader.ready()) continue; - if (m_reader.type() != MessageType::Key) + if (m_reader.type() == MessageType::Key) + { + auto key = m_reader.read(); + m_reader.reset(); + if (key.modifiers == Key::Modifiers::Resize) + m_dimensions = key.coord(); + m_on_key(key); + } + else if (m_reader.type() == MessageType::Paste) + { + auto content = m_reader.read(); + m_reader.reset(); + m_on_paste(content); + } + else { m_socket_watcher.close_fd(); return; } - - auto key = m_reader.read(); - m_reader.reset(); - if (key.modifiers == Key::Modifiers::Resize) - m_dimensions = key.coord(); - m_on_key(key); } } catch (const disconnected& err) @@ -660,6 +673,11 @@ RemoteClient::RemoteClient(StringView session, StringView name, std::unique_ptr< msg.write(key); m_socket_watcher->events() |= FdEvents::Write; }); + m_ui->set_on_paste([this](StringView content){ + MsgWriter msg(m_send_buffer, MessageType::Paste); + msg.write(content); + m_socket_watcher->events() |= FdEvents::Write; + }); m_socket_watcher.reset(new FDWatcher{sock, FdEvents::Read | FdEvents::Write, EventMode::Urgent, [this, reader = MsgReader{}](FDWatcher& watcher, FdEvents events, EventMode) mutable { diff --git a/src/terminal_ui.cc b/src/terminal_ui.cc index 311c2527..612029e8 100644 --- a/src/terminal_ui.cc +++ b/src/terminal_ui.cc @@ -1,5 +1,6 @@ #include "terminal_ui.hh" +#include "buffer_utils.hh" #include "display_buffer.hh" #include "event_manager.hh" #include "exception.hh" @@ -683,12 +684,16 @@ Optional TerminalUI::get_next_key() return resize(dimensions()); } - static auto get_char = []() -> Optional { + static auto get_char = [this]() -> Optional { if (not fd_readable(STDIN_FILENO)) return {}; if (unsigned char c = 0; read(STDIN_FILENO, &c, 1) == 1) + { + if (m_paste_buffer) + m_paste_buffer->push_back(c); return c; + } stdin_closed = 1; return {}; @@ -700,7 +705,7 @@ Optional TerminalUI::get_next_key() static constexpr auto control = [](char c) { return c & 037; }; - auto convert = [this](Codepoint c) -> Codepoint { + static auto convert = [this](Codepoint c) -> Codepoint { if (c == control('m') or c == control('j')) return Key::Return; if (c == control('i')) @@ -715,7 +720,7 @@ Optional TerminalUI::get_next_key() return Key::Escape; return c; }; - auto parse_key = [&convert](unsigned char c) -> Key { + static auto parse_key = [](unsigned char c) -> Key { if (Codepoint cp = convert(c); cp > 255) return Key{cp}; // Special case: you can type NUL with Ctrl-2 or Ctrl-Shift-2 or @@ -743,7 +748,7 @@ Optional TerminalUI::get_next_key() return Key{utf8::codepoint(CharIterator{c}, Sentinel{})}; }; - auto parse_mask = [](int mask) { + static auto parse_mask = [](int mask) { Key::Modifiers mod = Key::Modifiers::None; if (mask & 1) mod |= Key::Modifiers::Shift; @@ -754,7 +759,16 @@ Optional TerminalUI::get_next_key() return mod; }; - auto parse_csi = [this, &convert, &parse_mask]() -> Optional { + enum class PasteEvent { Begin, End }; + struct KeyOrPasteEvent { + KeyOrPasteEvent() = default; + KeyOrPasteEvent(Key key) : key(key) {} + KeyOrPasteEvent(Optional key) : key(key) {} + KeyOrPasteEvent(PasteEvent paste) : paste(paste) {} + const Optional key; + const Optional paste; + }; + auto parse_csi = [this]() -> KeyOrPasteEvent { auto next_char = [] { return get_char().value_or((unsigned char)0xff); }; int params[16][4] = {}; auto c = next_char(); @@ -819,7 +833,7 @@ Optional TerminalUI::get_next_key() { if (params[0][0] == 2026) m_synchronized.supported = (params[1][0] == 1 or params[1][0] == 2); - return {Key::Invalid}; + return Key{Key::Invalid}; } switch (params[0][0]) { @@ -863,6 +877,10 @@ Optional TerminalUI::get_next_key() return Key{Key::Modifiers::Shift, Key::F7 + params[0][0] - 31}; // rxvt style case 33: case 34: return Key{Key::Modifiers::Shift, Key::F9 + params[0][0] - 33}; // rxvt style + case 200: + return PasteEvent::Begin; + case 201: + return PasteEvent::End; } return {}; case 'u': @@ -899,7 +917,7 @@ Optional TerminalUI::get_next_key() return {}; }; - auto parse_ss3 = [&parse_mask]() -> Optional { + static auto parse_ss3 = []() -> Optional { int raw_mask = 0; char code = '0'; do { @@ -944,17 +962,40 @@ Optional TerminalUI::get_next_key() } }; + if (m_paste_buffer) + { + if (*c == 27 and get_char() == '[' and parse_csi().paste == PasteEvent::End) + { + m_paste_buffer->resize(m_paste_buffer->length() - "\033[201~"_str.length(), '\0'); + m_on_paste(*m_paste_buffer); + m_paste_buffer.reset(); + } + return get_next_key(); + } + if (*c != 27) return parse_key(*c); if (auto next = get_char()) { - if (*next == '[') // potential CSI - return parse_csi().value_or(alt('[')); if (*next == 'O') // potential SS3 return parse_ss3().value_or(alt('O')); - return alt(parse_key(*next)); + if (*next != '[') + return alt(parse_key(*next)); + // potential CSI + KeyOrPasteEvent csi = parse_csi(); + if (csi.paste == PasteEvent::Begin) + { + m_paste_buffer = String{}; + return get_next_key(); + } + if (csi.paste == PasteEvent::End) // Unmatched bracketed paste sequence. + return {}; + if (csi.key) + return *csi.key; + return alt('['); } + return Key{Key::Escape}; } @@ -1395,6 +1436,11 @@ void TerminalUI::set_on_key(OnKeyCallback callback) EventManager::instance().force_signal(0); } +void TerminalUI::set_on_paste(OnPasteCallback callback) +{ + m_on_paste = std::move(callback); +} + DisplayCoord TerminalUI::dimensions() { return m_dimensions; @@ -1422,6 +1468,7 @@ void TerminalUI::setup_terminal() "\033[?25l" // hide cursor "\033=" // set application keypad mode, so the keypad keys send unique codes "\033[?2026$p" // query support for synchronize output + "\033[?2004h" // force enable bracketed-paste events ); } @@ -1435,6 +1482,7 @@ void TerminalUI::restore_terminal() "\033[>4;0m" "\033[?1004l" "\033[?1049l" + "\033[?2004l" "\033[m" // set the terminal output back to default colours and style ); } diff --git a/src/terminal_ui.hh b/src/terminal_ui.hh index fe3f1d6b..dd4ef870 100644 --- a/src/terminal_ui.hh +++ b/src/terminal_ui.hh @@ -55,6 +55,7 @@ public: DisplayCoord dimensions() override; void set_on_key(OnKeyCallback callback) override; + void set_on_paste(OnPasteCallback callback) override; void set_ui_options(const Options& options) override; static void setup_terminal(); @@ -137,6 +138,8 @@ private: FDWatcher m_stdin_watcher; OnKeyCallback m_on_key; + OnPasteCallback m_on_paste; + Optional m_paste_buffer; bool m_status_on_top = false; ConstArrayView m_assistant; diff --git a/src/user_interface.hh b/src/user_interface.hh index 5bab3e89..643f0d1d 100644 --- a/src/user_interface.hh +++ b/src/user_interface.hh @@ -43,6 +43,7 @@ enum class CursorMode }; using OnKeyCallback = std::function; +using OnPasteCallback = std::function; class UserInterface { @@ -78,6 +79,7 @@ public: virtual void refresh(bool force) = 0; virtual void set_on_key(OnKeyCallback callback) = 0; + virtual void set_on_paste(OnPasteCallback callback) = 0; using Options = HashMap; virtual void set_ui_options(const Options& options) = 0;