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
main
Johannes Altmanninger 2022-12-11 19:30:02 +01:00
parent ad36585b7a
commit b2cf74bb4a
10 changed files with 145 additions and 15 deletions

View File

@ -60,6 +60,9 @@ Client::Client(std::unique_ptr<UserInterface>&& 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());

View File

@ -32,6 +32,7 @@ public:
InputMode& operator=(const InputMode&) = delete;
void handle_key(Key key) { RefPtr<InputMode> 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<String> strings)
template<typename S>
void insert(ConstArrayView<S> 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<InputModes::Normal*>(&current_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)

View File

@ -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

View File

@ -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<JsonObject>())

View File

@ -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<Key, MemoryDomain::Client> m_pending_keys;
DisplayCoord m_dimensions;
String m_requests;

View File

@ -626,6 +626,7 @@ std::unique_ptr<UserInterface> 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 {}
};

View File

@ -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<Key>();
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<String>();
m_reader.reset();
m_on_paste(content);
}
else
{
m_socket_watcher.close_fd();
return;
}
auto key = m_reader.read<Key>();
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 {

View File

@ -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<Key> TerminalUI::get_next_key()
return resize(dimensions());
}
static auto get_char = []() -> Optional<unsigned char> {
static auto get_char = [this]() -> Optional<unsigned char> {
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<Key> TerminalUI::get_next_key()
return mod;
};
auto parse_csi = [this]() -> Optional<Key> {
enum class PasteEvent { Begin, End };
struct KeyOrPasteEvent {
KeyOrPasteEvent() = default;
KeyOrPasteEvent(Key key) : key(key) {}
KeyOrPasteEvent(Optional<Key> key) : key(key) {}
KeyOrPasteEvent(PasteEvent paste) : paste(paste) {}
const Optional<Key> key;
const Optional<PasteEvent> 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<Key> 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<Key> 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<Key> 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
);
}

View File

@ -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<String> m_paste_buffer;
bool m_status_on_top = false;
ConstArrayView<StringView> m_assistant;

View File

@ -43,6 +43,7 @@ enum class CursorMode
};
using OnKeyCallback = std::function<void(Key key)>;
using OnPasteCallback = std::function<void(StringView content)>;
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<String, String, MemoryDomain::Options>;
virtual void set_ui_options(const Options& options) = 0;