mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-28 13:53:10 +00:00
Use event_queue_peeker_t when matching key bindings
Previously, when attempting to match a key binding, we would dequeue events from the queue and put them back on if the binding fails. The tricky part is timeouts: distinguishing between an escaped character and the escape key itself. This was handled with "timeout events" and we had to be careful to know when to discard them. Switch to a new model: use event_queue_peeker more pervasively. Temporarily dequeued events are stored in the peeker, and the peeker itself remembers when it has seen a timeout. This is in preparation for removing the idea of "timeout events" altogether.
This commit is contained in:
parent
c570a14c04
commit
bd72791340
2 changed files with 148 additions and 100 deletions
243
src/input.cpp
243
src/input.cpp
|
@ -428,32 +428,6 @@ void inputter_t::mapping_execute(const input_mapping_t &m,
|
|||
if (!m.sets_mode.empty()) input_set_bind_mode(*parser_, m.sets_mode);
|
||||
}
|
||||
|
||||
/// Try reading the specified function mapping.
|
||||
bool inputter_t::mapping_is_match(const input_mapping_t &m) {
|
||||
const wcstring &str = m.seq;
|
||||
|
||||
assert(!str.empty() && "zero-length input string passed to mapping_is_match!");
|
||||
|
||||
bool timed = false;
|
||||
for (size_t i = 0; i < str.size(); ++i) {
|
||||
auto evt = timed ? event_queue_.readch_timed() : event_queue_.readch();
|
||||
if (!evt.is_char() || evt.get_char() != str[i]) {
|
||||
// We didn't match the bind sequence/input mapping, (it timed out or they entered
|
||||
// something else). Undo consumption of the read characters since we didn't match the
|
||||
// bind sequence and abort.
|
||||
event_queue_.push_front(evt);
|
||||
event_queue_.insert_front(str.begin(), str.begin() + i);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we just read an escape, we need to add a timeout for the next char,
|
||||
// to distinguish between the actual escape key and an "alt"-modifier.
|
||||
timed = (str[i] == L'\x1B');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void inputter_t::queue_ch(const char_event_t &ch) {
|
||||
if (ch.is_readline()) {
|
||||
function_push_args(ch.get_readline());
|
||||
|
@ -463,71 +437,94 @@ void inputter_t::queue_ch(const char_event_t &ch) {
|
|||
|
||||
void inputter_t::push_front(const char_event_t &ch) { event_queue_.push_front(ch); }
|
||||
|
||||
/// \return the first mapping that matches, walking first over the user's mapping list, then the
|
||||
/// preset list. \return null if nothing matches.
|
||||
maybe_t<input_mapping_t> inputter_t::find_mapping() {
|
||||
const input_mapping_t *generic = nullptr;
|
||||
const auto &vars = parser_->vars();
|
||||
const wcstring bind_mode = input_get_bind_mode(vars);
|
||||
|
||||
auto ml = input_mappings()->all_mappings();
|
||||
for (const auto &m : *ml) {
|
||||
if (m.mode != bind_mode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.is_generic()) {
|
||||
if (!generic) generic = &m;
|
||||
} else if (mapping_is_match(m)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return generic ? maybe_t<input_mapping_t>(*generic) : none();
|
||||
}
|
||||
|
||||
/// A class which allows accumulating input events, or return them to the queue.
|
||||
/// A class which allows accumulating input events, or returns them to the queue.
|
||||
/// This contains a list of events which have been dequeued, and a current index into that list.
|
||||
class event_queue_peeker_t {
|
||||
public:
|
||||
explicit event_queue_peeker_t(input_event_queue_t &event_queue) : event_queue_(event_queue) {}
|
||||
|
||||
/// \return the next event, optionally waiting for it.
|
||||
char_event_t next(bool timed = false) {
|
||||
auto event = timed ? event_queue_.readch_timed() : event_queue_.readch();
|
||||
peeked_.push_back(event);
|
||||
return event;
|
||||
/// \return the next event.
|
||||
char_event_t next() {
|
||||
assert(idx_ <= peeked_.size() && "Index must not be larger than dequeued event count");
|
||||
if (idx_ == peeked_.size()) {
|
||||
auto event = event_queue_.readch();
|
||||
peeked_.push_back(event);
|
||||
}
|
||||
return peeked_.at(idx_++);
|
||||
}
|
||||
|
||||
/// \return how many events are currently stored.
|
||||
size_t len() const { return peeked_.size(); }
|
||||
|
||||
/// Consume all events that have been peeked, leaving this empty.
|
||||
void consume() { peeked_.clear(); }
|
||||
|
||||
/// Return all peeked events to the queue.
|
||||
void restart() {
|
||||
event_queue_.insert_front(peeked_.cbegin(), peeked_.cend());
|
||||
peeked_.clear();
|
||||
}
|
||||
|
||||
~event_queue_peeker_t() { restart(); }
|
||||
|
||||
private:
|
||||
std::vector<char_event_t> peeked_;
|
||||
input_event_queue_t &event_queue_;
|
||||
};
|
||||
|
||||
bool inputter_t::have_mouse_tracking_csi() {
|
||||
// Maximum length of any CSI is NPAR (which is nominally 16), although this does not account for
|
||||
// user input intermixed with pseudo input generated by the tty emulator.
|
||||
event_queue_peeker_t peeker(event_queue_);
|
||||
|
||||
// Check for the CSI first
|
||||
if (peeker.next().maybe_char() != L'\x1B'
|
||||
|| peeker.next(true /* timed */).maybe_char() != L'[') {
|
||||
/// Check if the next event is the given character. This advances the index on success only.
|
||||
/// If \p timed is set, then return false if this (or any other) character had a timeout.
|
||||
bool next_is_char(wchar_t c, bool timed = false) {
|
||||
assert(idx_ <= peeked_.size() && "Index must not be larger than dequeued event count");
|
||||
// See if we had a timeout already.
|
||||
if (timed && had_timeout_) {
|
||||
return false;
|
||||
}
|
||||
// Grab a new event if we have exhausted what we have already peeked.
|
||||
if (idx_ == peeked_.size()) {
|
||||
auto newevt = timed ? event_queue_.readch_timed() : event_queue_.readch();
|
||||
if (newevt.is_timeout()) {
|
||||
assert(timed && "Should only get timeouts from timed reads");
|
||||
had_timeout_ = true;
|
||||
return false;
|
||||
}
|
||||
peeked_.push_back(newevt);
|
||||
}
|
||||
// Now we have peeked far enough, check the event.
|
||||
// If it matches the char, then increment the index.
|
||||
if (peeked_.at(idx_).maybe_char() == c) {
|
||||
idx_++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto next = peeker.next().maybe_char();
|
||||
/// \return the current index.
|
||||
size_t len() const { return idx_; }
|
||||
|
||||
/// Consume all events up to the current index.
|
||||
/// Remaining events are returned to the queue.
|
||||
void consume() {
|
||||
event_queue_.insert_front(peeked_.cbegin() + idx_, peeked_.cend());
|
||||
peeked_.clear();
|
||||
idx_ = 0;
|
||||
}
|
||||
|
||||
/// Reset our index back to 0.
|
||||
void restart() { idx_ = 0; }
|
||||
|
||||
~event_queue_peeker_t() {
|
||||
assert(idx_ == 0 && "Events left on the queue - missing restart or consume?");
|
||||
consume();
|
||||
}
|
||||
|
||||
private:
|
||||
/// The list of events which have been dequeued.
|
||||
std::vector<char_event_t> peeked_{};
|
||||
|
||||
/// If set, then some previous timed event timed out.
|
||||
bool had_timeout_{false};
|
||||
|
||||
/// The current index. This never exceeds peeked_.size().
|
||||
size_t idx_{0};
|
||||
|
||||
/// The queue from which to read more events.
|
||||
input_event_queue_t &event_queue_;
|
||||
};
|
||||
|
||||
/// Try reading a mouse-tracking CSI sequence, using the given \p peeker.
|
||||
/// Events are left on the peeker and the caller must restart or consume it.
|
||||
/// \return true if matched, false if not.
|
||||
static bool have_mouse_tracking_csi(event_queue_peeker_t *peeker) {
|
||||
// Maximum length of any CSI is NPAR (which is nominally 16), although this does not account for
|
||||
// user input intermixed with pseudo input generated by the tty emulator.
|
||||
// Check for the CSI first.
|
||||
if (!peeker->next_is_char(L'\x1b') || !peeker->next_is_char(L'[', true /* timed */)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto next = peeker->next().maybe_char();
|
||||
size_t length = 0;
|
||||
if (next == L'M') {
|
||||
// Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6 chars
|
||||
|
@ -538,13 +535,13 @@ bool inputter_t::have_mouse_tracking_csi() {
|
|||
// Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters for button
|
||||
// code, Px, and Py, ending with 'M' for button press or 'm' for button release.
|
||||
while (true) {
|
||||
next = peeker.next().maybe_char();
|
||||
next = peeker->next().maybe_char();
|
||||
if (next == L'M' || next == L'm') {
|
||||
// However much we've read, we've consumed the CSI in its entirety.
|
||||
length = peeker.len();
|
||||
length = peeker->len();
|
||||
break;
|
||||
}
|
||||
if (peeker.len() == 16) {
|
||||
if (peeker->len() >= 16) {
|
||||
// This is likely a malformed mouse-reporting CSI but we can't do anything about it.
|
||||
return false;
|
||||
}
|
||||
|
@ -561,18 +558,61 @@ bool inputter_t::have_mouse_tracking_csi() {
|
|||
|
||||
// Consume however many characters it takes to prevent the mouse tracking sequence from reaching
|
||||
// the prompt, dependent on the class of mouse reporting as detected above.
|
||||
while (peeker.len() < length) {
|
||||
(void)peeker.next();
|
||||
while (peeker->len() < length) {
|
||||
(void)peeker->next();
|
||||
}
|
||||
|
||||
peeker.consume();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// \return true if a given \p peeker matches a given sequence of char events given by \p str.
|
||||
static bool try_peek_sequence(event_queue_peeker_t *peeker, const wcstring &str) {
|
||||
assert(!str.empty() && "Empty string passed to try_peek_sequence");
|
||||
wchar_t prev = L'\0';
|
||||
for (wchar_t c : str) {
|
||||
// If we just read an escape, we need to add a timeout for the next char,
|
||||
// to distinguish between the actual escape key and an "alt"-modifier.
|
||||
bool timed = prev == L'\x1B';
|
||||
if (!peeker->next_is_char(c, timed)) {
|
||||
return false;
|
||||
}
|
||||
prev = c;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// \return the first mapping that matches, walking first over the user's mapping list, then the
|
||||
/// preset list. \return null if nothing matches.
|
||||
maybe_t<input_mapping_t> inputter_t::find_mapping(event_queue_peeker_t *peeker) {
|
||||
const input_mapping_t *generic = nullptr;
|
||||
const auto &vars = parser_->vars();
|
||||
const wcstring bind_mode = input_get_bind_mode(vars);
|
||||
|
||||
auto ml = input_mappings()->all_mappings();
|
||||
for (const auto &m : *ml) {
|
||||
if (m.mode != bind_mode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Defer generic mappings until the end.
|
||||
if (m.is_generic()) {
|
||||
if (!generic) generic = &m;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try_peek_sequence(peeker, m.seq)) {
|
||||
return m;
|
||||
}
|
||||
peeker->restart();
|
||||
}
|
||||
return generic ? maybe_t<input_mapping_t>(*generic) : none();
|
||||
}
|
||||
|
||||
void inputter_t::mapping_execute_matching_or_generic(const command_handler_t &command_handler) {
|
||||
event_queue_peeker_t peeker(event_queue_);
|
||||
|
||||
// Check for mouse-tracking CSI before mappings to prevent the generic mapping handler from
|
||||
// taking over.
|
||||
if (have_mouse_tracking_csi()) {
|
||||
if (have_mouse_tracking_csi(&peeker)) {
|
||||
// fish recognizes but does not actually support mouse reporting. We never turn it on, and
|
||||
// it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn
|
||||
// it off before exiting. We swallow the events to prevent garbage from piling up at the
|
||||
|
@ -588,17 +628,26 @@ void inputter_t::mapping_execute_matching_or_generic(const command_handler_t &co
|
|||
// We can't/shouldn't directly manipulate stdout from `input.cpp`, so request the execution
|
||||
// of a helper function to disable mouse tracking.
|
||||
// writembs(outputter_t::stdoutput(), "\x1B[?1000l");
|
||||
peeker.consume();
|
||||
event_queue_.push_front(char_event_t(readline_cmd_t::disable_mouse_tracking, L""));
|
||||
return;
|
||||
}
|
||||
else if (auto mapping = find_mapping()) {
|
||||
peeker.restart();
|
||||
|
||||
// Check for ordinary mappings.
|
||||
if (auto mapping = find_mapping(&peeker)) {
|
||||
peeker.consume();
|
||||
mapping_execute(*mapping, command_handler);
|
||||
} else {
|
||||
FLOGF(reader, L"no generic found, ignoring char...");
|
||||
auto evt = event_queue_.readch();
|
||||
if (evt.is_eof()) {
|
||||
event_queue_.push_front(evt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
peeker.restart();
|
||||
|
||||
FLOGF(reader, L"no generic found, ignoring char...");
|
||||
auto evt = peeker.next();
|
||||
if (evt.is_eof()) {
|
||||
event_queue_.push_front(evt);
|
||||
}
|
||||
peeker.consume();
|
||||
}
|
||||
|
||||
/// Helper function. Picks through the queue of incoming characters until we get to one that's not a
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#define FISH_BIND_MODE_VAR L"fish_bind_mode"
|
||||
#define DEFAULT_BIND_MODE L"default"
|
||||
|
||||
class event_queue_peeker_t;
|
||||
class parser_t;
|
||||
|
||||
wcstring describe_char(wint_t c);
|
||||
|
@ -67,9 +68,7 @@ class inputter_t {
|
|||
void function_push_args(readline_cmd_t code);
|
||||
void mapping_execute(const input_mapping_t &m, const command_handler_t &command_handler);
|
||||
void mapping_execute_matching_or_generic(const command_handler_t &command_handler);
|
||||
bool mapping_is_match(const input_mapping_t &m);
|
||||
bool have_mouse_tracking_csi();
|
||||
maybe_t<input_mapping_t> find_mapping();
|
||||
maybe_t<input_mapping_t> find_mapping(event_queue_peeker_t *peeker);
|
||||
char_event_t read_characters_no_readline();
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue