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
This commit is contained in:
parent
ad36585b7a
commit
b2cf74bb4a
|
@ -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());
|
||||
|
||||
|
|
|
@ -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*>(¤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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user