From ad36585b7ad236bea7d1c02b0679ae371c3c2a9e Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 11 Mar 2023 16:19:15 +0100 Subject: [PATCH 1/3] Make TerminalUI::get_next_key() helpers static The only depend on the TerminalUI object which is a singleton, so we can make them all static. --- src/terminal_ui.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/terminal_ui.cc b/src/terminal_ui.cc index 311c2527..1df9480c 100644 --- a/src/terminal_ui.cc +++ b/src/terminal_ui.cc @@ -700,7 +700,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 +715,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 +743,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 +754,7 @@ Optional TerminalUI::get_next_key() return mod; }; - auto parse_csi = [this, &convert, &parse_mask]() -> Optional { + auto parse_csi = [this]() -> Optional { auto next_char = [] { return get_char().value_or((unsigned char)0xff); }; int params[16][4] = {}; auto c = next_char(); @@ -899,7 +899,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 { From b2cf74bb4a8286c5a191c54e947c0b2c9bb7cf96 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sun, 11 Dec 2022 19:30:02 +0100 Subject: [PATCH 2/3] Implement bracketed paste Text pasted into Kakoune's normal mode is interpreted as command sequence, which is probably never what the user wants. Text pasted during insert mode will be inserted fine but may trigger auto-indentation hooks which is likely not what users want. Bracketed paste is pair of escape codes sent by terminals that allow applications to distinguish between pasted text and typed text. Let's use this feature to always insert pasted text verbatim, skipping keymap lookup and the InsertChar hook. In future, we could add a dedicated Paste hook. We need to make a decision on whether to paste before or after the selection. I chose "before" because that's what I'm used to. TerminalUI::set_on_key has EventManager::instance().force_signal(0); I'm not sure if we want the same for TerminalUI::set_on_paste? I assume it doesn't matter because they are always called in tandem. Closes #2465 --- src/client.cc | 3 +++ src/input_handler.cc | 50 ++++++++++++++++++++++++++++++++++-- src/input_handler.hh | 2 ++ src/json_ui.cc | 5 ++++ src/json_ui.hh | 2 ++ src/main.cc | 1 + src/remote.cc | 32 ++++++++++++++++++----- src/terminal_ui.cc | 60 ++++++++++++++++++++++++++++++++++++++----- src/terminal_ui.hh | 3 +++ src/user_interface.hh | 2 ++ 10 files changed, 145 insertions(+), 15 deletions(-) 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..a24ddfa2 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,28 @@ private: InputHandler& m_input_handler; }; +void InputMode::paste(StringView content) +{ + try + { + Buffer& buffer = context().buffer(); + ScopedEdition edition{context()}; + ScopedSelectionEdition selection_edition{context()}; + context().selections().for_each([&buffer, content=std::move(content)] + (size_t index, Selection& sel) { + BufferRange range = buffer.insert(sel.min(), content); + sel.min() = range.begin; + sel.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 +1038,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 +1467,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 +1502,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 +1515,7 @@ private: void insert(Codepoint key) { String str{key}; - insert(str); + insert(ConstArrayView{str}); context().hooks().run_hook(Hook::InsertChar, str, context()); } @@ -1644,6 +1685,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/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 1df9480c..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 {}; @@ -754,7 +759,16 @@ Optional TerminalUI::get_next_key() return mod; }; - auto parse_csi = [this]() -> 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': @@ -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; From 1990a764e3d2ffa77931068f876cd49f76bd43f9 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 9 Mar 2023 21:21:29 +0100 Subject: [PATCH 3/3] Make linewise bracketed paste match P behavior This is experimental. Testing will reveal if this is the desired behavior. --- src/input_handler.cc | 12 ++++++++---- src/normal.cc | 7 ------- src/normal.hh | 10 ++++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/input_handler.cc b/src/input_handler.cc index a24ddfa2..877517e2 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -77,13 +77,17 @@ 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)] + context().selections().for_each([&buffer, content=std::move(content), linewise] (size_t index, Selection& sel) { - BufferRange range = buffer.insert(sel.min(), content); - sel.min() = range.begin; - sel.max() = range.end > range.begin ? buffer.char_prev(range.end) : range.begin; + 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) 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