2016-05-03 17:36:22 +00:00
|
|
|
// Functions for reading data from stdin and passing to the parser. If stdin is a keyboard, it
|
|
|
|
// supplies a killring, history, syntax highlighting, tab-completion and various other interactive
|
|
|
|
// features.
|
|
|
|
//
|
|
|
|
// Internally the interactive mode functions rely in the functions of the input library to read
|
|
|
|
// individual characters of input.
|
|
|
|
//
|
|
|
|
// Token search is handled incrementally. Actual searches are only done on when searching backwards,
|
|
|
|
// since the previous results are saved. The last search position is remembered and a new search
|
|
|
|
// continues from the last search position. All search results are saved in the list 'search_prev'.
|
|
|
|
// When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search
|
|
|
|
// result, and subsequent backwards searches are also handled by consulting the list up until the
|
|
|
|
// end of the list is reached, at which point regular searching will commence.
|
2005-09-20 13:26:39 +00:00
|
|
|
#include "config.h"
|
|
|
|
|
2016-04-21 06:00:54 +00:00
|
|
|
// IWYU pragma: no_include <type_traits>
|
2016-05-03 17:36:22 +00:00
|
|
|
#include <errno.h>
|
2016-12-20 04:35:57 +00:00
|
|
|
#include <fcntl.h>
|
2016-05-03 17:36:22 +00:00
|
|
|
#include <pthread.h>
|
2016-12-20 04:35:57 +00:00
|
|
|
#ifdef HAVE_SIGINFO_H
|
|
|
|
#include <siginfo.h>
|
|
|
|
#endif
|
|
|
|
#include <signal.h>
|
2005-09-20 13:26:39 +00:00
|
|
|
#include <stdio.h>
|
2016-05-03 17:36:22 +00:00
|
|
|
#include <stdlib.h>
|
2019-10-13 22:50:48 +00:00
|
|
|
|
2019-03-12 22:07:07 +00:00
|
|
|
#include <cstring>
|
2016-12-20 04:35:57 +00:00
|
|
|
#ifdef HAVE_SYS_SELECT_H
|
|
|
|
#include <sys/select.h>
|
|
|
|
#endif
|
2016-05-03 17:36:22 +00:00
|
|
|
#include <sys/time.h>
|
2016-12-20 04:35:57 +00:00
|
|
|
#include <sys/types.h>
|
2005-09-20 13:26:39 +00:00
|
|
|
#include <termios.h>
|
2006-11-30 23:57:49 +00:00
|
|
|
#include <time.h>
|
2005-09-20 13:26:39 +00:00
|
|
|
#include <unistd.h>
|
|
|
|
#include <wctype.h>
|
2016-12-20 04:35:57 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
#include <algorithm>
|
2017-03-26 20:44:27 +00:00
|
|
|
#include <atomic>
|
2017-02-11 02:47:02 +00:00
|
|
|
#include <csignal>
|
2019-10-13 22:50:48 +00:00
|
|
|
#include <cwchar>
|
2017-02-11 02:47:02 +00:00
|
|
|
#include <functional>
|
2016-04-21 06:00:54 +00:00
|
|
|
#include <memory>
|
2018-08-11 21:30:10 +00:00
|
|
|
#include <set>
|
2017-01-27 04:00:43 +00:00
|
|
|
#include <stack>
|
2005-12-25 22:00:44 +00:00
|
|
|
|
2020-06-21 00:28:31 +00:00
|
|
|
#include "ast.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "color.h"
|
2005-09-20 13:26:39 +00:00
|
|
|
#include "common.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "complete.h"
|
2005-09-20 13:26:39 +00:00
|
|
|
#include "env.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "event.h"
|
2005-09-20 13:26:39 +00:00
|
|
|
#include "exec.h"
|
|
|
|
#include "expand.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "fallback.h" // IWYU pragma: keep
|
2019-05-27 22:56:53 +00:00
|
|
|
#include "flog.h"
|
2005-09-20 13:26:39 +00:00
|
|
|
#include "function.h"
|
2019-04-29 01:13:55 +00:00
|
|
|
#include "global_safety.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "highlight.h"
|
|
|
|
#include "history.h"
|
|
|
|
#include "input.h"
|
|
|
|
#include "input_common.h"
|
2012-02-02 23:05:08 +00:00
|
|
|
#include "intern.h"
|
2016-04-21 06:00:54 +00:00
|
|
|
#include "io.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "iothread.h"
|
|
|
|
#include "kill.h"
|
|
|
|
#include "output.h"
|
|
|
|
#include "pager.h"
|
2016-04-21 06:00:54 +00:00
|
|
|
#include "parse_constants.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "parse_util.h"
|
|
|
|
#include "parser.h"
|
|
|
|
#include "proc.h"
|
|
|
|
#include "reader.h"
|
|
|
|
#include "sanity.h"
|
|
|
|
#include "screen.h"
|
|
|
|
#include "signal.h"
|
2020-06-08 02:35:47 +00:00
|
|
|
#include "termsize.h"
|
2016-05-03 17:36:22 +00:00
|
|
|
#include "tokenizer.h"
|
|
|
|
#include "wutil.h" // IWYU pragma: keep
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2017-01-07 05:14:29 +00:00
|
|
|
// Name of the variable that tells how long it took, in milliseconds, for the previous
|
|
|
|
// interactive command to complete.
|
2018-11-22 00:42:34 +00:00
|
|
|
#define ENV_CMD_DURATION L"CMD_DURATION"
|
2017-01-07 05:14:29 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
/// Maximum length of prefix string when printing completion list. Longer prefixes will be
|
|
|
|
/// ellipsized.
|
|
|
|
#define PREFIX_MAX_LEN 9
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// A simple prompt for reading shell commands that does not rely on fish specific commands, meaning
|
|
|
|
/// it will work even if fish is not installed. This is used by read_i.
|
2018-05-12 11:53:45 +00:00
|
|
|
#define DEFAULT_PROMPT L"echo -n \"$USER@$hostname $PWD \"'> '"
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The name of the function that prints the fish prompt.
|
2012-11-08 03:59:20 +00:00
|
|
|
#define LEFT_PROMPT_FUNCTION_NAME L"fish_prompt"
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The name of the function that prints the fish right prompt (RPROMPT).
|
2012-11-08 03:59:20 +00:00
|
|
|
#define RIGHT_PROMPT_FUNCTION_NAME L"fish_right_prompt"
|
|
|
|
|
2017-06-20 05:57:32 +00:00
|
|
|
/// The name of the function to use in place of the left prompt if we're in the debugger context.
|
|
|
|
#define DEBUG_PROMPT_FUNCTION_NAME L"fish_breakpoint_prompt"
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The name of the function for getting the input mode indicator.
|
2015-06-04 20:31:48 +00:00
|
|
|
#define MODE_PROMPT_FUNCTION_NAME L"fish_mode_prompt"
|
|
|
|
|
2021-02-28 07:40:39 +00:00
|
|
|
/// The default title for the reader. This is used by reader_readline.
|
|
|
|
#define DEFAULT_TITLE L"echo (status current-command) \" \" $PWD"
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The maximum number of characters to read from the keyboard without repainting. Note that this
|
|
|
|
/// readahead will only occur if new characters are available for reading, fish will never block for
|
|
|
|
/// more input without repainting.
|
2019-03-18 06:37:46 +00:00
|
|
|
static constexpr size_t READAHEAD_MAX = 256;
|
2005-10-13 14:08:33 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// A mode for calling the reader_kill function. In this mode, the new string is appended to the
|
|
|
|
/// current contents of the kill buffer.
|
2006-10-12 13:27:32 +00:00
|
|
|
#define KILL_APPEND 0
|
2016-05-03 17:36:22 +00:00
|
|
|
|
|
|
|
/// A mode for calling the reader_kill function. In this mode, the new string is prepended to the
|
|
|
|
/// current contents of the kill buffer.
|
2006-10-12 13:27:32 +00:00
|
|
|
#define KILL_PREPEND 1
|
|
|
|
|
2018-02-03 22:10:47 +00:00
|
|
|
enum class history_search_direction_t { forward, backward };
|
2007-04-16 21:26:15 +00:00
|
|
|
|
2018-08-11 07:05:49 +00:00
|
|
|
enum class jump_direction_t { forward, backward };
|
|
|
|
enum class jump_precision_t { till, to };
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Any time the contents of a buffer changes, we update the generation count. This allows for our
|
2017-03-26 20:44:27 +00:00
|
|
|
/// background threads to notice it and skip doing work that they would otherwise have to do.
|
2020-08-22 19:30:33 +00:00
|
|
|
static std::atomic<uint32_t> s_generation;
|
2013-01-22 23:19:29 +00:00
|
|
|
|
2017-03-26 20:44:27 +00:00
|
|
|
/// Helper to get the generation count
|
2020-08-22 19:30:33 +00:00
|
|
|
static inline uint32_t read_generation_count() {
|
2019-02-24 21:39:43 +00:00
|
|
|
return s_generation.load(std::memory_order_relaxed);
|
2017-03-26 20:44:27 +00:00
|
|
|
}
|
|
|
|
|
2020-01-16 01:14:47 +00:00
|
|
|
/// \return an operation context for a background operation..
|
|
|
|
/// Crucially the operation context itself does not contain a parser.
|
|
|
|
/// It is the caller's responsibility to ensure the environment lives as long as the result.
|
2020-09-08 20:04:44 +00:00
|
|
|
static operation_context_t get_bg_context(const std::shared_ptr<environment_t> &env,
|
|
|
|
uint32_t generation_count) {
|
2020-01-16 01:14:47 +00:00
|
|
|
cancel_checker_t cancel_checker = [generation_count] {
|
|
|
|
// Cancel if the generation count changed.
|
|
|
|
return generation_count != read_generation_count();
|
|
|
|
};
|
2020-12-20 19:58:26 +00:00
|
|
|
return operation_context_t{nullptr, *env, std::move(cancel_checker), kExpansionLimitBackground};
|
2020-01-16 01:14:47 +00:00
|
|
|
}
|
|
|
|
|
2020-11-22 13:39:48 +00:00
|
|
|
/// We try to ensure that syntax highlighting completes appropriately before executing what the user
|
|
|
|
/// typed. But we do not want it to block forever - e.g. it may hang on determining if an arbitrary
|
|
|
|
/// argument is a path. This is how long we'll wait (in milliseconds) before giving up and
|
|
|
|
/// performing a no-io syntax highlighting. See #7418, #5912.
|
2020-11-02 02:25:09 +00:00
|
|
|
static constexpr long kHighlightTimeoutForExecutionMs = 250;
|
|
|
|
|
2020-03-03 08:13:33 +00:00
|
|
|
/// Get the debouncer for autosuggestions and background highlighting.
|
|
|
|
/// These are deliberately leaked to avoid shutdown dtor registration.
|
|
|
|
static debounce_t &debounce_autosuggestions() {
|
|
|
|
const long kAutosuggetTimeoutMs = 500;
|
2020-04-02 23:04:04 +00:00
|
|
|
static auto res = new debounce_t(kAutosuggetTimeoutMs);
|
2020-03-03 08:13:33 +00:00
|
|
|
return *res;
|
|
|
|
}
|
|
|
|
|
|
|
|
static debounce_t &debounce_highlighting() {
|
|
|
|
const long kHighlightTimeoutMs = 500;
|
2020-04-02 23:04:04 +00:00
|
|
|
static auto res = new debounce_t(kHighlightTimeoutMs);
|
2020-03-03 08:13:33 +00:00
|
|
|
return *res;
|
|
|
|
}
|
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
bool edit_t::operator==(const edit_t &other) const {
|
|
|
|
return cursor_position_before_edit == other.cursor_position_before_edit &&
|
|
|
|
offset == other.offset && length == other.length && old == other.old &&
|
|
|
|
replacement == other.replacement;
|
|
|
|
}
|
|
|
|
|
|
|
|
void undo_history_t::clear() {
|
|
|
|
edits.clear();
|
|
|
|
edits_applied = 0;
|
|
|
|
may_coalesce = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void apply_edit(wcstring *target, const edit_t &edit) {
|
|
|
|
target->replace(edit.offset, edit.length, edit.replacement);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the number of characters left of the cursor that are removed by the
|
|
|
|
/// deletion in the given edit.
|
|
|
|
static size_t chars_deleted_left_of_cursor(const edit_t &edit) {
|
|
|
|
if (edit.cursor_position_before_edit > edit.offset) {
|
|
|
|
return std::min(edit.length, edit.cursor_position_before_edit - edit.offset);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Compute the position of the cursor after the given edit.
|
|
|
|
static size_t cursor_position_after_edit(const edit_t &edit) {
|
|
|
|
size_t cursor = edit.cursor_position_before_edit + edit.replacement.size();
|
|
|
|
size_t removed = chars_deleted_left_of_cursor(edit);
|
|
|
|
return cursor > removed ? cursor - removed : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Whether we want to append this string to the previous edit.
|
|
|
|
static bool want_to_coalesce_insertion_of(const editable_line_t &el, const wcstring &str) {
|
|
|
|
// The previous edit must support coalescing.
|
|
|
|
if (!el.undo_history.may_coalesce) return false;
|
|
|
|
// Only consolidate single character inserts.
|
|
|
|
if (str.size() != 1) return false;
|
|
|
|
// Make an undo group after every space.
|
2021-01-05 21:40:09 +00:00
|
|
|
if (str.at(0) == L' ' && !el.undo_history.try_coalesce) return false;
|
2020-02-04 11:47:44 +00:00
|
|
|
assert(!el.undo_history.edits.empty());
|
|
|
|
const edit_t &last_edit = el.undo_history.edits.back();
|
|
|
|
// Don't add to the last edit if it deleted something.
|
|
|
|
if (last_edit.length != 0) return false;
|
|
|
|
// Must not have moved the cursor!
|
|
|
|
if (cursor_position_after_edit(last_edit) != el.position()) return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool editable_line_t::undo() {
|
2021-01-05 21:40:09 +00:00
|
|
|
bool did_undo = false;
|
|
|
|
maybe_t<int> last_group_id{-1};
|
|
|
|
while (undo_history.edits_applied != 0) {
|
|
|
|
const edit_t &edit = undo_history.edits.at(undo_history.edits_applied - 1);
|
|
|
|
if (did_undo && (!edit.group_id.has_value() || edit.group_id != last_group_id)) {
|
|
|
|
// We've restored all the edits in this logical undo group
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
last_group_id = edit.group_id;
|
|
|
|
undo_history.edits_applied--;
|
|
|
|
edit_t inverse = edit_t(edit.offset, edit.replacement.size(), L"");
|
|
|
|
inverse.replacement = edit.old;
|
|
|
|
size_t old_position = edit.cursor_position_before_edit;
|
|
|
|
apply_edit(&text_, inverse);
|
|
|
|
set_position(old_position);
|
|
|
|
did_undo = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
end_edit_group();
|
2020-02-04 11:47:44 +00:00
|
|
|
undo_history.may_coalesce = false;
|
2021-01-05 21:40:09 +00:00
|
|
|
return did_undo;
|
2020-02-04 11:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void editable_line_t::push_edit(edit_t &&edit) {
|
2021-01-05 21:40:09 +00:00
|
|
|
// Assign a new group id or propagate the old one if we're in a logical grouping of edits
|
|
|
|
if (edit_group_level_ != -1) {
|
|
|
|
edit.group_id = edit_group_id_;
|
|
|
|
}
|
|
|
|
|
2020-02-21 07:10:39 +00:00
|
|
|
bool edit_does_nothing = edit.length == 0 && edit.replacement.empty();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (edit_does_nothing) return;
|
|
|
|
if (undo_history.edits_applied != undo_history.edits.size()) {
|
|
|
|
// After undoing some edits, the user is making a new edit;
|
|
|
|
// we are about to create a new edit branch.
|
|
|
|
// Discard all edits that were undone because we only support
|
|
|
|
// linear undo/redo, they will be unreachable.
|
|
|
|
undo_history.edits.erase(undo_history.edits.begin() + undo_history.edits_applied,
|
|
|
|
undo_history.edits.end());
|
|
|
|
}
|
|
|
|
edit.cursor_position_before_edit = position();
|
|
|
|
edit.old = text_.substr(edit.offset, edit.length);
|
|
|
|
apply_edit(&text_, edit);
|
|
|
|
set_position(cursor_position_after_edit(edit));
|
|
|
|
assert(undo_history.edits_applied == undo_history.edits.size());
|
|
|
|
undo_history.edits_applied++;
|
|
|
|
undo_history.edits.emplace_back(edit);
|
|
|
|
}
|
|
|
|
|
2020-02-09 17:39:14 +00:00
|
|
|
void editable_line_t::insert_coalesce(const wcstring &str) {
|
|
|
|
edit_t &edit = undo_history.edits.back();
|
|
|
|
edit.replacement.append(str);
|
|
|
|
apply_edit(&text_, edit_t(position(), 0, str));
|
|
|
|
set_position(position() + str.size());
|
|
|
|
}
|
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
bool editable_line_t::redo() {
|
2021-01-05 21:40:09 +00:00
|
|
|
bool did_redo = false;
|
|
|
|
|
|
|
|
maybe_t<int> last_group_id{-1};
|
|
|
|
while (undo_history.edits_applied < undo_history.edits.size()) {
|
|
|
|
const edit_t &edit = undo_history.edits.at(undo_history.edits_applied);
|
|
|
|
if (did_redo && (!edit.group_id.has_value() || edit.group_id != last_group_id)) {
|
|
|
|
// We've restored all the edits in this logical undo group
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
last_group_id = edit.group_id;
|
|
|
|
undo_history.edits_applied++;
|
|
|
|
apply_edit(&text_, edit);
|
|
|
|
set_position(cursor_position_after_edit(edit));
|
|
|
|
did_redo = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
end_edit_group();
|
|
|
|
return did_redo;
|
|
|
|
}
|
|
|
|
|
|
|
|
void editable_line_t::begin_edit_group() {
|
|
|
|
if (++edit_group_level_ == 0) {
|
|
|
|
// Indicate that the next change must trigger the creation of a new history item
|
|
|
|
undo_history.may_coalesce = false;
|
|
|
|
// Indicate that future changes should be coalesced into the same edit if possible.
|
|
|
|
undo_history.try_coalesce = true;
|
|
|
|
// Assign a logical edit group id to future edits in this group
|
|
|
|
edit_group_id_ += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void editable_line_t::end_edit_group() {
|
|
|
|
if (edit_group_level_ == -1) {
|
|
|
|
// Clamp the minimum value to -1 to prevent unbalanced end_edit_group() calls from breaking
|
|
|
|
// everything.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (--edit_group_level_ == -1) {
|
|
|
|
undo_history.try_coalesce = false;
|
|
|
|
undo_history.may_coalesce = false;
|
|
|
|
}
|
2014-01-26 22:47:15 +00:00
|
|
|
}
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
/// Encapsulation of the reader's history search functionality.
|
|
|
|
class reader_history_search_t {
|
|
|
|
public:
|
|
|
|
enum mode_t {
|
|
|
|
inactive, // no search
|
|
|
|
line, // searching by line
|
2019-09-27 15:56:47 +00:00
|
|
|
prefix, // searching by prefix
|
2018-08-11 21:30:10 +00:00
|
|
|
token // searching by token
|
|
|
|
};
|
|
|
|
|
|
|
|
private:
|
|
|
|
/// The type of search performed.
|
|
|
|
mode_t mode_{inactive};
|
|
|
|
|
|
|
|
/// Our history search itself.
|
|
|
|
history_search_t search_;
|
|
|
|
|
|
|
|
/// The ordered list of matches. This may grow long.
|
2020-02-09 17:39:14 +00:00
|
|
|
std::vector<wcstring> matches_;
|
2018-08-11 21:30:10 +00:00
|
|
|
|
|
|
|
/// A set of new items to skip, corresponding to matches_ and anything added in skip().
|
|
|
|
std::set<wcstring> skips_;
|
|
|
|
|
|
|
|
/// Index into our matches list.
|
|
|
|
size_t match_index_{0};
|
|
|
|
|
|
|
|
/// Adds the given match if we haven't seen it before.
|
|
|
|
void add_if_new(wcstring text) {
|
|
|
|
if (add_skip(text)) {
|
|
|
|
matches_.push_back(std::move(text));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to append matches from the current history item.
|
|
|
|
/// \return true if something was appended.
|
|
|
|
bool append_matches_from_search() {
|
|
|
|
const size_t before = matches_.size();
|
|
|
|
wcstring text = search_.current_string();
|
2019-09-27 15:56:47 +00:00
|
|
|
if (mode_ == line || mode_ == prefix) {
|
2018-08-11 21:30:10 +00:00
|
|
|
add_if_new(std::move(text));
|
|
|
|
} else if (mode_ == token) {
|
|
|
|
const wcstring &needle = search_string();
|
|
|
|
tokenizer_t tok(text.c_str(), TOK_ACCEPT_UNFINISHED);
|
2018-08-16 19:07:38 +00:00
|
|
|
|
2019-03-14 18:15:50 +00:00
|
|
|
wcstring_list_t local_tokens;
|
2019-10-13 23:06:16 +00:00
|
|
|
while (auto token = tok.next()) {
|
|
|
|
if (token->type != token_type_t::string) continue;
|
|
|
|
wcstring text = tok.text_of(*token);
|
2018-08-11 21:30:10 +00:00
|
|
|
if (text.find(needle) != wcstring::npos) {
|
2018-08-16 19:07:38 +00:00
|
|
|
local_tokens.emplace_back(std::move(text));
|
2018-08-11 21:30:10 +00:00
|
|
|
}
|
|
|
|
}
|
2018-08-16 19:07:38 +00:00
|
|
|
|
|
|
|
// Make sure tokens are added in reverse order. See #5150
|
|
|
|
for (auto i = local_tokens.rbegin(); i != local_tokens.rend(); ++i) {
|
|
|
|
add_if_new(std::move(*i));
|
|
|
|
}
|
2018-08-11 21:30:10 +00:00
|
|
|
}
|
|
|
|
return matches_.size() > before;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool move_forwards() {
|
|
|
|
// Try to move within our previously discovered matches.
|
|
|
|
if (match_index_ > 0) {
|
|
|
|
match_index_--;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool move_backwards() {
|
|
|
|
// Try to move backwards within our previously discovered matches.
|
|
|
|
if (match_index_ + 1 < matches_.size()) {
|
|
|
|
match_index_++;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add more items from our search.
|
|
|
|
while (search_.go_backwards()) {
|
|
|
|
if (append_matches_from_search()) {
|
|
|
|
match_index_++;
|
|
|
|
assert(match_index_ < matches_.size() && "Should have found more matches");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Here we failed to go backwards past the last history item.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
|
|
|
reader_history_search_t() = default;
|
|
|
|
~reader_history_search_t() = default;
|
|
|
|
|
|
|
|
bool active() const { return mode_ != inactive; }
|
|
|
|
|
|
|
|
bool by_token() const { return mode_ == token; }
|
|
|
|
|
|
|
|
bool by_line() const { return mode_ == line; }
|
|
|
|
|
2019-09-27 15:56:47 +00:00
|
|
|
bool by_prefix() const { return mode_ == prefix; }
|
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
/// Move the history search in the given direction \p dir.
|
|
|
|
bool move_in_direction(history_search_direction_t dir) {
|
|
|
|
return dir == history_search_direction_t::forward ? move_forwards() : move_backwards();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Go to the beginning (earliest) of the search.
|
|
|
|
void go_to_beginning() {
|
2020-02-09 17:39:14 +00:00
|
|
|
if (matches_.empty()) return;
|
|
|
|
match_index_ = matches_.size() - 1;
|
2018-08-11 21:30:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Go to the end (most recent) of the search.
|
|
|
|
void go_to_end() { match_index_ = 0; }
|
|
|
|
|
|
|
|
/// \return the current search result.
|
|
|
|
const wcstring ¤t_result() const {
|
|
|
|
assert(match_index_ < matches_.size() && "Invalid match index");
|
|
|
|
return matches_.at(match_index_);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// \return the string we are searching for.
|
2018-08-12 07:30:57 +00:00
|
|
|
const wcstring &search_string() const { return search_.original_term(); }
|
2018-08-11 21:30:10 +00:00
|
|
|
|
|
|
|
/// \return whether we are at the end (most recent) of our search.
|
|
|
|
bool is_at_end() const { return match_index_ == 0; }
|
|
|
|
|
|
|
|
// Add an item to skip.
|
|
|
|
// \return true if it was added, false if already present.
|
|
|
|
bool add_skip(const wcstring &str) { return skips_.insert(str).second; }
|
|
|
|
|
|
|
|
/// Reset, beginning a new line or token mode search.
|
2021-01-10 00:22:32 +00:00
|
|
|
void reset_to_mode(const wcstring &text, const std::shared_ptr<history_t> &hist, mode_t mode) {
|
2018-08-11 21:30:10 +00:00
|
|
|
assert(mode != inactive && "mode cannot be inactive in this setter");
|
|
|
|
skips_ = {text};
|
|
|
|
matches_ = {text};
|
|
|
|
match_index_ = 0;
|
|
|
|
mode_ = mode;
|
2020-09-22 14:11:39 +00:00
|
|
|
history_search_flags_t flags = history_search_no_dedup;
|
|
|
|
// Make the search case-insensitive unless we have an uppercase character.
|
|
|
|
wcstring low = wcstolower(text);
|
|
|
|
if (low == text) flags |= history_search_ignore_case;
|
2018-08-12 07:30:57 +00:00
|
|
|
// We can skip dedup in history_search_t because we do it ourselves in skips_.
|
2019-09-27 15:56:47 +00:00
|
|
|
search_ = history_search_t(
|
2021-01-10 00:22:32 +00:00
|
|
|
hist, text,
|
2020-11-22 13:39:48 +00:00
|
|
|
by_prefix() ? history_search_type_t::prefix : history_search_type_t::contains, flags);
|
2018-08-11 21:30:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Reset to inactive search.
|
|
|
|
void reset() {
|
|
|
|
matches_.clear();
|
|
|
|
skips_.clear();
|
|
|
|
match_index_ = 0;
|
|
|
|
mode_ = inactive;
|
|
|
|
search_ = history_search_t();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-11-27 22:41:35 +00:00
|
|
|
/// The result of an autosuggestion computation.
|
|
|
|
struct autosuggestion_t {
|
|
|
|
// The text to use, as an extension of the command line.
|
2020-11-27 23:00:16 +00:00
|
|
|
wcstring text{};
|
2020-11-27 22:41:35 +00:00
|
|
|
|
|
|
|
// The string which was searched for.
|
2020-11-27 23:00:16 +00:00
|
|
|
wcstring search_string{};
|
|
|
|
|
|
|
|
// Whether the autosuggestion should be case insensitive.
|
|
|
|
// This is true for file-generated autosuggestions, but not for history.
|
|
|
|
bool icase{false};
|
2020-11-27 22:41:35 +00:00
|
|
|
|
|
|
|
// Clear our contents.
|
|
|
|
void clear() {
|
|
|
|
text.clear();
|
|
|
|
search_string.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
// \return whether we have empty text.
|
|
|
|
bool empty() const { return text.empty(); }
|
2020-11-27 23:00:16 +00:00
|
|
|
|
|
|
|
autosuggestion_t() = default;
|
|
|
|
autosuggestion_t(wcstring text, wcstring search_string, bool icase)
|
|
|
|
: text(std::move(text)), search_string(std::move(search_string)), icase(icase) {}
|
2019-03-03 22:14:08 +00:00
|
|
|
};
|
|
|
|
|
2019-03-03 22:34:52 +00:00
|
|
|
struct highlight_result_t {
|
|
|
|
std::vector<highlight_spec_t> colors;
|
|
|
|
wcstring text;
|
|
|
|
};
|
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
} // namespace
|
|
|
|
|
2019-03-18 06:37:46 +00:00
|
|
|
struct readline_loop_state_t;
|
|
|
|
|
2020-08-11 21:29:52 +00:00
|
|
|
/// Data wrapping up the visual selection.
|
|
|
|
struct selection_data_t {
|
|
|
|
/// The position of the cursor when selection was initiated.
|
|
|
|
size_t begin{0};
|
|
|
|
|
|
|
|
/// The start and stop position of the current selection.
|
|
|
|
size_t start{0};
|
|
|
|
size_t stop{0};
|
2020-08-23 22:12:47 +00:00
|
|
|
|
|
|
|
bool operator==(const selection_data_t &rhs) const {
|
|
|
|
return begin == rhs.begin && start == rhs.start && stop == rhs.stop;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool operator!=(const selection_data_t &rhs) const { return !(*this == rhs); }
|
|
|
|
};
|
|
|
|
|
|
|
|
/// A value-type struct representing a layout from which we can call to s_write().
|
|
|
|
/// The intent is that everything we send to the screen is encapsulated in this struct.
|
|
|
|
struct layout_data_t {
|
|
|
|
/// Text of the command line.
|
|
|
|
wcstring text{};
|
|
|
|
|
|
|
|
/// The colors. This has the same length as 'text'.
|
|
|
|
std::vector<highlight_spec_t> colors{};
|
|
|
|
|
|
|
|
/// Position of the cursor in the command line.
|
|
|
|
size_t position{};
|
|
|
|
|
|
|
|
/// Whether the cursor is focused on the pager or not.
|
|
|
|
bool focused_on_pager{false};
|
|
|
|
|
|
|
|
/// Visual selection of the command line, or none if none.
|
|
|
|
maybe_t<selection_data_t> selection{};
|
|
|
|
|
|
|
|
/// String containing the autosuggestion.
|
|
|
|
wcstring autosuggestion{};
|
|
|
|
|
|
|
|
/// String containing the history search. If non-empty, then highlight the found range within
|
|
|
|
/// the text.
|
|
|
|
wcstring history_search_text{};
|
|
|
|
|
|
|
|
/// The result of evaluating the left, mode and right prompt commands.
|
|
|
|
/// That is, this the text of the prompts, not the commands to produce them.
|
|
|
|
wcstring left_prompt_buff{};
|
|
|
|
wcstring mode_prompt_buff{};
|
|
|
|
wcstring right_prompt_buff{};
|
2020-08-11 21:29:52 +00:00
|
|
|
};
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// A struct describing the state of the interactive reader. These states can be stacked, in case
|
|
|
|
/// reader_readline() calls are nested. This happens when the 'read' builtin is used.
|
2019-03-03 22:21:15 +00:00
|
|
|
class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
|
2016-05-03 17:36:22 +00:00
|
|
|
public:
|
2020-08-03 21:53:09 +00:00
|
|
|
/// Configuration for the reader.
|
|
|
|
const reader_config_t conf;
|
2019-05-27 01:51:26 +00:00
|
|
|
/// The parser being used.
|
|
|
|
std::shared_ptr<parser_t> parser_ref;
|
2016-05-03 17:36:22 +00:00
|
|
|
/// String containing the whole current commandline.
|
2014-01-26 08:41:30 +00:00
|
|
|
editable_line_t command_line;
|
2020-02-04 11:47:44 +00:00
|
|
|
/// Whether the most recent modification to the command line was done by either history search
|
|
|
|
/// or a pager selection change. When this is true and another transient change is made, the
|
|
|
|
/// old transient change will be removed from the undo history.
|
|
|
|
bool command_line_has_transient_edit = false;
|
2020-08-23 22:12:47 +00:00
|
|
|
/// The most recent layout data sent to the screen.
|
|
|
|
layout_data_t rendered_layout;
|
2020-11-27 22:41:35 +00:00
|
|
|
/// The current autosuggestion.
|
|
|
|
autosuggestion_t autosuggestion;
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Current pager.
|
2014-01-20 07:52:35 +00:00
|
|
|
pager_t pager;
|
2020-08-23 22:12:47 +00:00
|
|
|
/// The output of the pager.
|
2014-01-17 20:04:03 +00:00
|
|
|
page_rendering_t current_page_rendering;
|
2016-05-03 17:36:22 +00:00
|
|
|
/// When backspacing, we temporarily suppress autosuggestions.
|
2018-08-11 19:39:24 +00:00
|
|
|
bool suppress_autosuggestion{false};
|
2020-08-23 22:12:47 +00:00
|
|
|
|
2021-02-24 20:00:56 +00:00
|
|
|
/// HACK: A flag to reset the loop state from the outside.
|
|
|
|
bool reset_loop_state{false};
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The representation of the current screen contents.
|
2012-11-19 00:30:30 +00:00
|
|
|
screen_t screen;
|
2020-08-23 22:12:47 +00:00
|
|
|
|
2019-06-02 22:41:23 +00:00
|
|
|
/// The source of input events.
|
|
|
|
inputter_t inputter;
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The history.
|
2021-01-10 00:22:32 +00:00
|
|
|
std::shared_ptr<history_t> history{};
|
2018-08-11 21:30:10 +00:00
|
|
|
/// The history search.
|
|
|
|
reader_history_search_t history_search{};
|
2020-08-11 21:29:52 +00:00
|
|
|
|
|
|
|
/// The selection data. If this is not none, then we have an active selection.
|
|
|
|
maybe_t<selection_data_t> selection{};
|
|
|
|
|
2012-11-19 00:30:30 +00:00
|
|
|
wcstring left_prompt_buff;
|
2019-04-01 13:52:21 +00:00
|
|
|
wcstring mode_prompt_buff;
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The output of the last evaluation of the right prompt command.
|
2012-11-19 00:30:30 +00:00
|
|
|
wcstring right_prompt_buff;
|
2020-08-23 22:12:47 +00:00
|
|
|
|
|
|
|
/// When navigating the pager, we modify the command line.
|
|
|
|
/// This is the saved command line before modification.
|
2014-01-27 08:56:13 +00:00
|
|
|
wcstring cycle_command_line;
|
2018-08-11 19:39:24 +00:00
|
|
|
size_t cycle_cursor_pos{0};
|
2020-08-23 22:12:47 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
/// If set, a key binding or the 'exit' command has asked us to exit our read loop.
|
|
|
|
bool exit_loop_requested{false};
|
2016-05-03 17:36:22 +00:00
|
|
|
/// If this is true, exit reader even if there are running jobs. This happens if we press e.g.
|
|
|
|
/// ^D twice.
|
2020-08-15 21:41:11 +00:00
|
|
|
bool did_warn_for_bg_jobs{false};
|
2016-05-03 17:36:22 +00:00
|
|
|
/// The current contents of the top item in the kill ring.
|
2012-11-19 00:30:30 +00:00
|
|
|
wcstring kill_item;
|
2020-08-23 22:12:47 +00:00
|
|
|
|
|
|
|
/// A flag which may be set to force re-execing all prompts and re-rendering.
|
|
|
|
/// This may come about when a color like $fish_color... has changed.
|
|
|
|
bool force_exec_prompt_and_repaint{false};
|
|
|
|
|
2018-08-11 07:05:49 +00:00
|
|
|
/// The target character of the last jump command.
|
|
|
|
wchar_t last_jump_target{0};
|
|
|
|
jump_direction_t last_jump_direction{jump_direction_t::forward};
|
|
|
|
jump_precision_t last_jump_precision{jump_precision_t::to};
|
2013-01-20 21:23:27 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
/// The text of the most recent asynchronous highlight and autosuggestion requests.
|
|
|
|
/// If these differs from the text of the command line, then we must kick off a new request.
|
|
|
|
wcstring in_flight_highlight_request;
|
|
|
|
wcstring in_flight_autosuggest_request;
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
bool is_navigating_pager_contents() const { return this->pager.is_navigating_contents(); }
|
|
|
|
|
|
|
|
/// The line that is currently being edited. Typically the command line, but may be the search
|
|
|
|
/// field.
|
2019-03-03 23:00:29 +00:00
|
|
|
const editable_line_t *active_edit_line() const {
|
2016-05-03 17:36:22 +00:00
|
|
|
if (this->is_navigating_pager_contents() && this->pager.is_search_field_shown()) {
|
|
|
|
return &this->pager.search_field_line;
|
|
|
|
}
|
2016-05-04 22:19:47 +00:00
|
|
|
return &this->command_line;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
editable_line_t *active_edit_line() {
|
2020-04-02 23:04:04 +00:00
|
|
|
auto cthis = reinterpret_cast<const reader_data_t *>(this);
|
2019-03-03 23:00:29 +00:00
|
|
|
return const_cast<editable_line_t *>(cthis->active_edit_line());
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Do what we need to do whenever our command line changes.
|
|
|
|
void command_line_changed(const editable_line_t *el);
|
|
|
|
|
2019-09-09 22:01:39 +00:00
|
|
|
/// Do what we need to do whenever our pager selection changes.
|
2016-05-03 17:36:22 +00:00
|
|
|
void pager_selection_changed();
|
|
|
|
|
|
|
|
/// Expand abbreviations at the current cursor position, minus backtrack_amt.
|
2019-03-03 21:59:35 +00:00
|
|
|
bool expand_abbreviation_as_necessary(size_t cursor_backtrack);
|
2016-05-03 17:36:22 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
/// \return the string used for history search, or an empty string if none.
|
|
|
|
wcstring history_search_text_if_active() const;
|
|
|
|
|
|
|
|
/// \return true if the command line has changed and repainting is needed. If \p colors is not
|
|
|
|
/// null, then also return true if the colors have changed.
|
|
|
|
using highlight_list_t = std::vector<highlight_spec_t>;
|
|
|
|
bool is_repaint_needed(const highlight_list_t *mcolors = nullptr) const;
|
|
|
|
|
|
|
|
/// Generate a new layout data from the current state of the world.
|
|
|
|
/// If \p mcolors has a value, then apply it; otherwise extend existing colors.
|
|
|
|
layout_data_t make_layout_data(maybe_t<highlight_list_t> mcolors = none()) const;
|
|
|
|
|
|
|
|
/// Generate a new layout data from the current state of the world, and paint with it.
|
|
|
|
/// If \p mcolors has a value, then apply it; otherwise extend existing colors.
|
|
|
|
void layout_and_repaint(const wchar_t *reason, maybe_t<highlight_list_t> mcolors = none()) {
|
|
|
|
this->rendered_layout = make_layout_data(std::move(mcolors));
|
|
|
|
paint_layout(reason);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Paint the last rendered layout.
|
|
|
|
/// \p reason is used in FLOG to explain why.
|
|
|
|
void paint_layout(const wchar_t *reason);
|
2019-03-03 23:47:00 +00:00
|
|
|
|
2018-09-14 07:36:26 +00:00
|
|
|
/// Return the variable set used for e.g. command duration.
|
2019-05-27 01:51:26 +00:00
|
|
|
env_stack_t &vars() { return parser_ref->vars(); }
|
|
|
|
const env_stack_t &vars() const { return parser_ref->vars(); }
|
2018-09-14 07:36:26 +00:00
|
|
|
|
2019-05-27 01:51:26 +00:00
|
|
|
/// Access the parser.
|
|
|
|
parser_t &parser() { return *parser_ref; }
|
2018-09-22 04:52:47 +00:00
|
|
|
|
2021-01-10 00:22:32 +00:00
|
|
|
reader_data_t(std::shared_ptr<parser_t> parser, std::shared_ptr<history_t> hist,
|
|
|
|
reader_config_t &&conf)
|
2020-08-03 21:53:09 +00:00
|
|
|
: conf(std::move(conf)),
|
|
|
|
parser_ref(std::move(parser)),
|
2020-10-31 17:14:50 +00:00
|
|
|
inputter(*parser_ref, conf.in),
|
2021-01-10 00:22:32 +00:00
|
|
|
history(std::move(hist)) {}
|
2019-03-03 21:59:35 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
void update_buff_pos(editable_line_t *el, maybe_t<size_t> new_pos = none_t());
|
2020-08-23 22:12:47 +00:00
|
|
|
|
2019-03-03 21:59:35 +00:00
|
|
|
void kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv);
|
2020-02-09 17:39:14 +00:00
|
|
|
/// Inserts a substring of str given by start, len at the cursor position.
|
2020-08-23 22:12:47 +00:00
|
|
|
void insert_string(editable_line_t *el, const wcstring &str);
|
2020-02-09 17:39:14 +00:00
|
|
|
/// Erase @length characters starting at @offset.
|
|
|
|
void erase_substring(editable_line_t *el, size_t offset, size_t length);
|
|
|
|
/// Replace the text of length @length at @offset by @replacement.
|
2020-12-24 10:00:20 +00:00
|
|
|
void replace_substring(editable_line_t *el, size_t offset, size_t length, wcstring replacement);
|
2020-02-09 17:39:14 +00:00
|
|
|
void push_edit(editable_line_t *el, edit_t &&edit);
|
2019-03-03 21:59:35 +00:00
|
|
|
|
|
|
|
/// Insert the character into the command line buffer and print it to the screen using syntax
|
|
|
|
/// highlighting, etc.
|
2020-08-23 22:12:47 +00:00
|
|
|
void insert_char(editable_line_t *el, wchar_t c) { insert_string(el, wcstring{c}); }
|
|
|
|
|
|
|
|
/// Read a command to execute, respecting input bindings.
|
|
|
|
/// \return the command, or none if we were asked to cancel (e.g. SIGHUP).
|
|
|
|
maybe_t<wcstring> readline(int nchars);
|
2019-03-03 21:59:35 +00:00
|
|
|
|
|
|
|
void move_word(editable_line_t *el, bool move_right, bool erase, enum move_word_style_t style,
|
|
|
|
bool newv);
|
|
|
|
|
2020-11-15 20:26:06 +00:00
|
|
|
void run_input_command_scripts(const wcstring_list_t &cmds);
|
2019-03-18 06:37:46 +00:00
|
|
|
maybe_t<char_event_t> read_normal_chars(readline_loop_state_t &rls);
|
2019-03-19 03:06:16 +00:00
|
|
|
void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls);
|
2019-03-03 22:09:23 +00:00
|
|
|
|
2019-04-28 21:06:03 +00:00
|
|
|
void select_completion_in_direction(selection_motion_t dir);
|
2019-03-03 23:47:00 +00:00
|
|
|
void flash();
|
2019-03-03 22:09:23 +00:00
|
|
|
|
2020-11-15 14:24:37 +00:00
|
|
|
void completion_insert(const wcstring &val, size_t token_end, complete_flags_t flags);
|
2019-03-03 23:00:29 +00:00
|
|
|
|
|
|
|
bool can_autosuggest() const;
|
2020-11-27 22:41:35 +00:00
|
|
|
void autosuggest_completed(autosuggestion_t result);
|
2019-03-03 22:14:08 +00:00
|
|
|
void update_autosuggestion();
|
2020-08-03 21:53:09 +00:00
|
|
|
void accept_autosuggestion(bool full, bool single = false,
|
|
|
|
move_word_style_t style = move_word_style_punctuation);
|
2020-11-02 02:25:09 +00:00
|
|
|
void super_highlight_me_plenty();
|
|
|
|
|
|
|
|
/// Finish up any outstanding syntax highlighting, before execution.
|
|
|
|
/// This plays some tricks to not block on I/O for too long.
|
|
|
|
void finish_highlighting_before_exec();
|
2019-03-03 22:34:52 +00:00
|
|
|
|
|
|
|
void highlight_complete(highlight_result_t result);
|
2019-04-01 13:52:21 +00:00
|
|
|
void exec_mode_prompt();
|
2019-03-03 22:38:53 +00:00
|
|
|
void exec_prompt();
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
bool jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el,
|
|
|
|
wchar_t target);
|
|
|
|
|
2020-04-21 19:41:21 +00:00
|
|
|
bool handle_completions(const completion_list_t &comp, size_t token_begin, size_t token_end);
|
2019-03-03 23:00:29 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
void set_command_line_and_position(editable_line_t *el, wcstring &&new_str, size_t pos);
|
|
|
|
void clear_transient_edit();
|
|
|
|
void replace_current_token(wcstring &&new_token);
|
2019-03-03 23:00:29 +00:00
|
|
|
void update_command_line_from_history_search();
|
2020-02-04 11:47:44 +00:00
|
|
|
void set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient = false);
|
|
|
|
void delete_char(bool backward = true);
|
2020-06-08 02:35:47 +00:00
|
|
|
|
|
|
|
/// Called to update the termsize, including $COLUMNS and $LINES, as necessary.
|
|
|
|
void update_termsize() { (void)termsize_container_t::shared().updating(parser()); }
|
2020-08-03 22:41:12 +00:00
|
|
|
|
|
|
|
// Import history from older location (config path) if our current history is empty.
|
|
|
|
void import_history_if_necessary();
|
2019-03-03 23:00:29 +00:00
|
|
|
};
|
2014-01-20 20:38:56 +00:00
|
|
|
|
2020-01-14 23:20:04 +00:00
|
|
|
/// This variable is set to a signal by the signal handler when ^C is pressed.
|
2017-01-21 21:33:46 +00:00
|
|
|
static volatile sig_atomic_t interrupted = 0;
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Prototypes for a bunch of functions defined later on.
|
2019-09-19 06:32:40 +00:00
|
|
|
static bool is_backslashed(const wcstring &str, size_t pos);
|
2013-02-02 22:50:22 +00:00
|
|
|
static wchar_t unescaped_quote(const wcstring &str, size_t pos);
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Mode on startup, which we restore on exit.
|
2013-09-23 01:11:53 +00:00
|
|
|
static struct termios terminal_mode_on_startup;
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Mode we use to execute programs.
|
2016-11-03 04:54:57 +00:00
|
|
|
static struct termios tty_modes_for_external_cmds;
|
2013-09-23 01:11:53 +00:00
|
|
|
|
2020-05-16 10:39:34 +00:00
|
|
|
/// Restore terminal settings we care about, to prevent a broken shell.
|
|
|
|
static void term_fix_modes(struct termios *modes) {
|
2020-06-24 18:43:56 +00:00
|
|
|
modes->c_iflag &= ~ICRNL; // disable mapping CR (\cM) to NL (\cJ)
|
|
|
|
modes->c_iflag &= ~INLCR; // disable mapping NL (\cJ) to CR (\cM)
|
2020-05-16 10:39:34 +00:00
|
|
|
modes->c_lflag &= ~ICANON; // turn off canonical mode
|
|
|
|
modes->c_lflag &= ~ECHO; // turn off echo mode
|
|
|
|
modes->c_lflag &= ~IEXTEN; // turn off handling of discard and lnext characters
|
2020-06-24 18:43:56 +00:00
|
|
|
modes->c_oflag |= OPOST; // turn on "implementation-defined post processing" - this often
|
|
|
|
// changes how line breaks work.
|
2021-01-18 20:00:08 +00:00
|
|
|
modes->c_oflag |= ONLCR; // "translate newline to carriage return-newline" - without
|
|
|
|
// you see staircase output.
|
2020-06-24 15:21:58 +00:00
|
|
|
|
|
|
|
modes->c_cc[VMIN] = 1;
|
|
|
|
modes->c_cc[VTIME] = 0;
|
|
|
|
|
2020-07-02 05:33:31 +00:00
|
|
|
unsigned char disabling_char = '\0';
|
|
|
|
// Prefer to use _POSIX_VDISABLE to disable control functions.
|
|
|
|
// This permits separately binding nul (typically control-space).
|
|
|
|
// POSIX calls out -1 as a special value which should be ignored.
|
|
|
|
#ifdef _POSIX_VDISABLE
|
|
|
|
if (_POSIX_VDISABLE != -1) disabling_char = _POSIX_VDISABLE;
|
|
|
|
#endif
|
|
|
|
|
2020-06-24 15:21:58 +00:00
|
|
|
// We ignore these anyway, so there is no need to sacrifice a character.
|
2020-07-02 05:33:31 +00:00
|
|
|
modes->c_cc[VSUSP] = disabling_char;
|
2020-05-16 10:39:34 +00:00
|
|
|
}
|
|
|
|
|
2020-09-27 11:53:04 +00:00
|
|
|
static void term_fix_external_modes(struct termios *modes) {
|
2021-01-18 20:00:08 +00:00
|
|
|
// Turning off OPOST or ONLCR breaks output (staircase effect), we don't allow it.
|
2020-09-27 11:53:04 +00:00
|
|
|
// See #7133.
|
|
|
|
modes->c_oflag |= OPOST;
|
2021-01-18 20:00:08 +00:00
|
|
|
modes->c_oflag |= ONLCR;
|
2020-09-27 11:53:04 +00:00
|
|
|
// These cause other ridiculous behaviors like input not being shown.
|
|
|
|
modes->c_lflag |= ICANON;
|
|
|
|
modes->c_lflag |= IEXTEN;
|
|
|
|
modes->c_lflag |= ECHO;
|
|
|
|
modes->c_iflag |= ICRNL;
|
|
|
|
modes->c_iflag &= ~INLCR;
|
|
|
|
}
|
2020-08-29 22:14:33 +00:00
|
|
|
/// A description of where fish is in the process of exiting.
|
|
|
|
enum class exit_state_t {
|
|
|
|
none, /// fish is not exiting.
|
|
|
|
running_handlers, /// fish intends to exit, and is running handlers like 'fish_exit'.
|
|
|
|
finished_handlers, /// fish is finished running handlers and no more fish script may be run.
|
|
|
|
};
|
|
|
|
static relaxed_atomic_t<exit_state_t> s_exit_state{exit_state_t::none};
|
2019-02-24 21:24:03 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
/// If set, SIGHUP has been received. This latches to true.
|
|
|
|
/// This is set from a signal handler.
|
|
|
|
static volatile sig_atomic_t s_sighup_received{false};
|
2019-02-24 21:24:03 +00:00
|
|
|
|
2020-07-06 04:17:21 +00:00
|
|
|
void reader_sighup() {
|
2020-08-15 21:41:11 +00:00
|
|
|
// Beware, we may be in a signal handler.
|
|
|
|
s_sighup_received = true;
|
2020-07-06 04:17:21 +00:00
|
|
|
}
|
|
|
|
|
2020-09-08 20:04:44 +00:00
|
|
|
static void redirect_tty_after_sighup() {
|
2020-08-15 21:41:11 +00:00
|
|
|
// If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or
|
|
|
|
// SIGTTOU.
|
|
|
|
assert(s_sighup_received && "SIGHUP not received");
|
2020-07-06 04:17:21 +00:00
|
|
|
static bool s_tty_redirected = false;
|
2020-08-15 21:41:11 +00:00
|
|
|
if (!s_tty_redirected) {
|
2020-07-06 03:09:53 +00:00
|
|
|
s_tty_redirected = true;
|
2020-08-15 21:41:11 +00:00
|
|
|
redirect_tty_output();
|
2020-07-06 03:09:53 +00:00
|
|
|
}
|
|
|
|
}
|
2006-05-14 10:16:23 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Give up control of terminal.
|
2021-03-06 08:24:54 +00:00
|
|
|
static void term_donate(bool quiet = false) {
|
2019-11-26 00:36:13 +00:00
|
|
|
while (true) {
|
2016-12-15 03:21:36 +00:00
|
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &tty_modes_for_external_cmds) == -1) {
|
2017-01-11 05:52:10 +00:00
|
|
|
if (errno == EIO) redirect_tty_output();
|
2016-05-03 17:36:22 +00:00
|
|
|
if (errno != EINTR) {
|
2021-03-06 08:24:54 +00:00
|
|
|
if (!quiet) {
|
|
|
|
FLOGF(warning, _(L"Could not set terminal mode for new job"));
|
|
|
|
wperror(L"tcsetattr");
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
break;
|
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
} else
|
2012-11-19 00:30:30 +00:00
|
|
|
break;
|
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 08:24:54 +00:00
|
|
|
/// Copy the (potentially changed) terminal modes and use them from now on.
|
|
|
|
void term_copy_modes() {
|
2020-05-16 10:39:34 +00:00
|
|
|
struct termios modes;
|
|
|
|
tcgetattr(STDIN_FILENO, &modes);
|
2020-06-24 18:43:56 +00:00
|
|
|
std::memcpy(&tty_modes_for_external_cmds, &modes, sizeof tty_modes_for_external_cmds);
|
2020-09-27 11:53:04 +00:00
|
|
|
term_fix_external_modes(&tty_modes_for_external_cmds);
|
2020-06-19 19:03:40 +00:00
|
|
|
|
2021-02-11 17:32:45 +00:00
|
|
|
// Copy flow control settings to shell modes.
|
|
|
|
if (tty_modes_for_external_cmds.c_iflag & IXON) {
|
|
|
|
shell_modes.c_iflag |= IXON;
|
|
|
|
} else {
|
|
|
|
shell_modes.c_iflag &= ~IXON;
|
|
|
|
}
|
|
|
|
if (tty_modes_for_external_cmds.c_iflag & IXOFF) {
|
|
|
|
shell_modes.c_iflag |= IXOFF;
|
|
|
|
} else {
|
|
|
|
shell_modes.c_iflag &= ~IXOFF;
|
|
|
|
}
|
2021-03-06 08:24:54 +00:00
|
|
|
}
|
2021-02-11 17:32:45 +00:00
|
|
|
|
2021-03-06 08:24:54 +00:00
|
|
|
/// Grab control of terminal.
|
|
|
|
void term_steal() {
|
|
|
|
term_copy_modes();
|
2019-11-26 00:36:13 +00:00
|
|
|
while (true) {
|
2016-12-15 03:21:36 +00:00
|
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes) == -1) {
|
2017-01-11 05:52:10 +00:00
|
|
|
if (errno == EIO) redirect_tty_output();
|
2016-05-03 17:36:22 +00:00
|
|
|
if (errno != EINTR) {
|
2020-01-19 12:38:47 +00:00
|
|
|
FLOGF(warning, _(L"Could not set terminal mode for shell"));
|
2016-12-15 03:21:36 +00:00
|
|
|
perror("tcsetattr");
|
2012-11-19 00:30:30 +00:00
|
|
|
break;
|
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
} else
|
2012-11-19 00:30:30 +00:00
|
|
|
break;
|
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-06-08 02:35:47 +00:00
|
|
|
termsize_container_t::shared().invalidate_tty();
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2020-08-29 22:14:33 +00:00
|
|
|
bool check_cancel_from_fish_signal() {
|
|
|
|
switch (s_exit_state) {
|
|
|
|
case exit_state_t::none:
|
2020-08-29 22:43:43 +00:00
|
|
|
// Cancel if we got SIGHUP.
|
|
|
|
return s_sighup_received;
|
2020-08-29 22:14:33 +00:00
|
|
|
case exit_state_t::running_handlers:
|
|
|
|
// We intend to exit but we want to allow these handlers to run.
|
|
|
|
return false;
|
|
|
|
case exit_state_t::finished_handlers:
|
|
|
|
// Done running exit handlers, time to exit.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
DIE("Unreachable");
|
|
|
|
}
|
2006-05-14 10:16:23 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Given a command line and an autosuggestion, return the string that gets shown to the user.
|
|
|
|
wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
|
|
|
|
const wcstring &autosuggestion) {
|
|
|
|
// We want to compute the full line, containing the command line and the autosuggestion They may
|
|
|
|
// disagree on whether characters are uppercase or lowercase Here we do something funny: if the
|
|
|
|
// last token of the command line contains any uppercase characters, we use its case. Otherwise
|
|
|
|
// we use the case of the autosuggestion. This is an idea from issue #335.
|
2013-01-05 23:21:42 +00:00
|
|
|
wcstring full_line;
|
2016-05-03 17:36:22 +00:00
|
|
|
if (autosuggestion.size() <= cmdline.size() || cmdline.empty()) {
|
|
|
|
// No or useless autosuggestion, or no command line.
|
2013-01-05 23:21:42 +00:00
|
|
|
full_line = cmdline;
|
2016-05-03 17:36:22 +00:00
|
|
|
} else if (string_prefixes_string(cmdline, autosuggestion)) {
|
|
|
|
// No case disagreements, or no extra characters in the autosuggestion.
|
2013-01-05 23:21:42 +00:00
|
|
|
full_line = autosuggestion;
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
|
|
|
// We have an autosuggestion which is not a prefix of the command line, i.e. a case
|
|
|
|
// disagreement. Decide whose case we want to use.
|
2019-11-19 02:34:50 +00:00
|
|
|
const wchar_t *begin = nullptr, *cmd = cmdline.c_str();
|
|
|
|
parse_util_token_extent(cmd, cmdline.size() - 1, &begin, nullptr, nullptr, nullptr);
|
2013-01-05 23:21:42 +00:00
|
|
|
bool last_token_contains_uppercase = false;
|
2016-05-03 17:36:22 +00:00
|
|
|
if (begin) {
|
2019-03-12 21:06:01 +00:00
|
|
|
const wchar_t *end = begin + std::wcslen(begin);
|
2013-01-05 23:21:42 +00:00
|
|
|
last_token_contains_uppercase = (std::find_if(begin, end, iswupper) != end);
|
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!last_token_contains_uppercase) {
|
|
|
|
// Use the autosuggestion's case.
|
2013-01-05 23:21:42 +00:00
|
|
|
full_line = autosuggestion;
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
|
|
|
// Use the command line case for its characters, then append the remaining characters in
|
|
|
|
// the autosuggestion. Note that we know that autosuggestion.size() > cmdline.size() due
|
|
|
|
// to the first test above.
|
2013-01-05 23:21:42 +00:00
|
|
|
full_line = cmdline;
|
2016-05-03 17:36:22 +00:00
|
|
|
full_line.append(autosuggestion, cmdline.size(),
|
|
|
|
autosuggestion.size() - cmdline.size());
|
2013-01-05 23:21:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return full_line;
|
|
|
|
}
|
|
|
|
|
2019-03-03 23:47:00 +00:00
|
|
|
/// Update the cursor position.
|
2020-02-04 11:47:44 +00:00
|
|
|
void reader_data_t::update_buff_pos(editable_line_t *el, maybe_t<size_t> new_pos) {
|
|
|
|
if (new_pos) {
|
|
|
|
el->set_position(*new_pos);
|
|
|
|
}
|
|
|
|
size_t buff_pos = el->position();
|
2020-08-11 21:29:52 +00:00
|
|
|
if (el == &command_line && selection.has_value()) {
|
|
|
|
if (selection->begin <= buff_pos) {
|
|
|
|
selection->start = selection->begin;
|
|
|
|
selection->stop = buff_pos + 1;
|
2019-03-03 23:47:00 +00:00
|
|
|
} else {
|
2020-08-11 21:29:52 +00:00
|
|
|
selection->start = buff_pos;
|
|
|
|
selection->stop = selection->begin + 1;
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
bool reader_data_t::is_repaint_needed(const std::vector<highlight_spec_t> *mcolors) const {
|
|
|
|
// Note: this function is responsible for detecting all of the ways that the command line may
|
|
|
|
// change, by comparing it to what is present in rendered_layout.
|
|
|
|
// The pager is the problem child, it has its own update logic.
|
|
|
|
auto check = [](bool val, const wchar_t *reason) {
|
|
|
|
if (val) FLOG(reader_render, L"repaint needed because", reason, L"change");
|
|
|
|
return val;
|
|
|
|
};
|
|
|
|
|
|
|
|
bool focused_on_pager = active_edit_line() == &pager.search_field_line;
|
|
|
|
const layout_data_t &last = this->rendered_layout;
|
|
|
|
return check(force_exec_prompt_and_repaint, L"forced") ||
|
|
|
|
check(command_line.text() != last.text, L"text") ||
|
|
|
|
check(mcolors && *mcolors != last.colors, L"highlight") ||
|
|
|
|
check(selection != last.selection, L"selection") ||
|
|
|
|
check(focused_on_pager != last.focused_on_pager, L"focus") ||
|
|
|
|
check(command_line.position() != last.position, L"position") ||
|
|
|
|
check(history_search_text_if_active() != last.history_search_text, L"history search") ||
|
2020-11-27 22:41:35 +00:00
|
|
|
check(autosuggestion.text != last.autosuggestion, L"autosuggestion") ||
|
2020-08-23 22:12:47 +00:00
|
|
|
check(left_prompt_buff != last.left_prompt_buff, L"left_prompt") ||
|
|
|
|
check(mode_prompt_buff != last.mode_prompt_buff, L"mode_prompt") ||
|
|
|
|
check(right_prompt_buff != last.right_prompt_buff, L"right_prompt") ||
|
|
|
|
check(pager.rendering_needs_update(current_page_rendering), L"pager");
|
|
|
|
}
|
|
|
|
|
|
|
|
layout_data_t reader_data_t::make_layout_data(maybe_t<highlight_list_t> mcolors) const {
|
|
|
|
layout_data_t result{};
|
|
|
|
bool focused_on_pager = active_edit_line() == &pager.search_field_line;
|
|
|
|
result.text = command_line.text();
|
|
|
|
|
|
|
|
if (mcolors.has_value()) {
|
|
|
|
result.colors = mcolors.acquire();
|
|
|
|
} else {
|
|
|
|
result.colors = rendered_layout.colors;
|
|
|
|
}
|
|
|
|
|
|
|
|
result.position = focused_on_pager ? pager.cursor_position() : command_line.position();
|
|
|
|
result.selection = selection;
|
|
|
|
result.focused_on_pager = (active_edit_line() == &pager.search_field_line);
|
|
|
|
result.history_search_text = history_search_text_if_active();
|
2020-11-27 22:41:35 +00:00
|
|
|
result.autosuggestion = autosuggestion.text;
|
2020-08-23 22:12:47 +00:00
|
|
|
result.left_prompt_buff = left_prompt_buff;
|
|
|
|
result.mode_prompt_buff = mode_prompt_buff;
|
|
|
|
result.right_prompt_buff = right_prompt_buff;
|
|
|
|
|
|
|
|
// Ensure our color list has the same length as the command line, by extending it with the last
|
|
|
|
// color. This typically reduces redraws; e.g. if the user continues types into an argument, we
|
|
|
|
// guess it's still an argument, while the highlighting proceeds in the background.
|
|
|
|
highlight_spec_t last_color = result.colors.empty() ? highlight_spec_t{} : result.colors.back();
|
|
|
|
result.colors.resize(result.text.size(), last_color);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::paint_layout(const wchar_t *reason) {
|
|
|
|
FLOGF(reader_render, L"Repainting from %ls", reason);
|
|
|
|
const layout_data_t &data = this->rendered_layout;
|
|
|
|
const editable_line_t *cmd_line = &command_line;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2017-04-09 18:48:21 +00:00
|
|
|
wcstring full_line;
|
2020-08-03 21:53:09 +00:00
|
|
|
if (conf.in_silent_mode) {
|
2020-02-04 11:47:44 +00:00
|
|
|
full_line = wcstring(cmd_line->text().length(), get_obfuscation_read_char());
|
2017-04-09 18:48:21 +00:00
|
|
|
} else {
|
|
|
|
// Combine the command and autosuggestion into one string.
|
2020-11-27 22:41:35 +00:00
|
|
|
full_line = combine_command_and_autosuggestion(cmd_line->text(), autosuggestion.text);
|
2017-04-09 18:48:21 +00:00
|
|
|
}
|
2013-01-08 10:39:22 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Copy the colors and extend them with autosuggestion color.
|
|
|
|
std::vector<highlight_spec_t> colors = data.colors;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Highlight any history search.
|
|
|
|
if (!conf.in_silent_mode && !data.history_search_text.empty()) {
|
|
|
|
const wcstring &needle = data.history_search_text;
|
|
|
|
const wcstring &haystack = cmd_line->text();
|
2020-11-22 13:39:48 +00:00
|
|
|
size_t match_pos = ifind(haystack, needle);
|
2020-08-23 22:12:47 +00:00
|
|
|
if (match_pos != wcstring::npos) {
|
|
|
|
for (size_t i = 0; i < needle.size(); i++) {
|
|
|
|
colors.at(match_pos + i).background = highlight_role_t::search_match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Apply any selection.
|
|
|
|
if (data.selection.has_value()) {
|
2019-03-04 01:34:00 +00:00
|
|
|
highlight_spec_t selection_color = {highlight_role_t::normal, highlight_role_t::selection};
|
2020-08-23 22:12:47 +00:00
|
|
|
for (size_t i = data.selection->start; i < std::min(selection->stop, colors.size()); i++) {
|
|
|
|
colors.at(i) = selection_color;
|
2014-03-29 21:19:45 +00:00
|
|
|
}
|
2014-01-15 14:07:22 +00:00
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Extend our colors with the autosuggestion.
|
|
|
|
colors.resize(full_line.size(), highlight_role_t::autosuggestion);
|
|
|
|
|
2020-08-11 20:40:42 +00:00
|
|
|
// Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion
|
|
|
|
// always conceptually has an indent of 0.
|
|
|
|
std::vector<int> indents = parse_util_compute_indents(cmd_line->text());
|
2020-08-23 22:12:47 +00:00
|
|
|
indents.resize(full_line.size(), 0);
|
2012-02-06 18:52:13 +00:00
|
|
|
|
2019-04-01 13:52:21 +00:00
|
|
|
// Prepend the mode prompt to the left prompt.
|
2020-07-05 06:38:03 +00:00
|
|
|
s_write(&screen, mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line,
|
2020-08-23 22:12:47 +00:00
|
|
|
cmd_line->size(), colors, indents, data.position, pager, current_page_rendering,
|
|
|
|
data.focused_on_pager);
|
2007-10-05 14:59:19 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Internal helper function for handling killing parts of text.
|
2019-03-03 21:59:35 +00:00
|
|
|
void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv) {
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *begin = el->text().c_str() + begin_idx;
|
2016-05-03 17:36:22 +00:00
|
|
|
if (newv) {
|
2019-03-03 21:59:35 +00:00
|
|
|
kill_item = wcstring(begin, length);
|
|
|
|
kill_add(kill_item);
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-03-03 21:59:35 +00:00
|
|
|
wcstring old = kill_item;
|
2016-05-03 17:36:22 +00:00
|
|
|
if (mode == KILL_APPEND) {
|
2019-03-03 21:59:35 +00:00
|
|
|
kill_item.append(begin, length);
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-03-03 21:59:35 +00:00
|
|
|
kill_item = wcstring(begin, length);
|
|
|
|
kill_item.append(old);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2019-03-03 21:59:35 +00:00
|
|
|
kill_replace(old, kill_item);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2020-02-09 17:39:14 +00:00
|
|
|
erase_substring(el, begin_idx, length);
|
2006-10-12 13:27:32 +00:00
|
|
|
}
|
2006-05-14 10:16:23 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// This is called from a signal handler!
|
2020-07-12 19:20:38 +00:00
|
|
|
void reader_handle_sigint() { interrupted = SIGINT; }
|
2005-10-05 22:37:08 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Make sure buffers are large enough to hold the current string length.
|
|
|
|
void reader_data_t::command_line_changed(const editable_line_t *el) {
|
2012-02-28 20:40:20 +00:00
|
|
|
ASSERT_IS_MAIN_THREAD();
|
2016-05-03 17:36:22 +00:00
|
|
|
if (el == &this->command_line) {
|
|
|
|
// Update the gen count.
|
2019-02-24 21:39:43 +00:00
|
|
|
s_generation.store(1 + read_generation_count(), std::memory_order_relaxed);
|
2016-05-03 17:36:22 +00:00
|
|
|
} else if (el == &this->pager.search_field_line) {
|
2014-01-27 08:56:13 +00:00
|
|
|
this->pager.refilter_completions();
|
2014-02-17 03:56:13 +00:00
|
|
|
this->pager_selection_changed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
void reader_data_t::pager_selection_changed() {
|
2014-02-17 03:56:13 +00:00
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
|
|
|
|
const completion_t *completion = this->pager.selected_completion(this->current_page_rendering);
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Update the cursor and command line.
|
2014-02-17 03:56:13 +00:00
|
|
|
size_t cursor_pos = this->cycle_cursor_pos;
|
|
|
|
wcstring new_cmd_line;
|
|
|
|
|
2019-11-19 02:34:50 +00:00
|
|
|
if (completion == nullptr) {
|
2014-02-17 03:56:13 +00:00
|
|
|
new_cmd_line = this->cycle_command_line;
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
|
|
|
new_cmd_line =
|
|
|
|
completion_apply_to_command_line(completion->completion, completion->flags,
|
|
|
|
this->cycle_command_line, &cursor_pos, false);
|
2014-02-17 03:56:13 +00:00
|
|
|
}
|
2020-02-04 11:47:44 +00:00
|
|
|
|
|
|
|
// Only update if something changed, to avoid useless edits in the undo history.
|
|
|
|
if (new_cmd_line != command_line.text()) {
|
|
|
|
set_buffer_maintaining_pager(new_cmd_line, cursor_pos, true /* transient */);
|
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Expand abbreviations at the given cursor position. Does NOT inspect 'data'.
|
2020-02-04 11:47:44 +00:00
|
|
|
maybe_t<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
|
|
|
|
const environment_t &vars) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// See if we are at "command position". Get the surrounding command substitution, and get the
|
|
|
|
// extent of the first token.
|
|
|
|
const wchar_t *const buff = cmdline.c_str();
|
2019-11-19 02:34:50 +00:00
|
|
|
const wchar_t *cmdsub_begin = nullptr, *cmdsub_end = nullptr;
|
2013-07-17 07:38:04 +00:00
|
|
|
parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end);
|
2019-11-19 02:34:50 +00:00
|
|
|
assert(cmdsub_begin != nullptr && cmdsub_begin >= buff);
|
|
|
|
assert(cmdsub_end != nullptr && cmdsub_end >= cmdsub_begin);
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Determine the offset of this command substitution.
|
2013-07-17 07:38:04 +00:00
|
|
|
const size_t subcmd_offset = cmdsub_begin - buff;
|
|
|
|
|
|
|
|
const wcstring subcmd = wcstring(cmdsub_begin, cmdsub_end - cmdsub_begin);
|
2013-10-09 09:03:50 +00:00
|
|
|
const size_t subcmd_cursor_pos = cursor_pos - subcmd_offset;
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Parse this subcmd.
|
2020-06-21 00:28:31 +00:00
|
|
|
using namespace ast;
|
|
|
|
auto ast =
|
|
|
|
ast_t::parse(subcmd, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens |
|
|
|
|
parse_flag_leave_unterminated);
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Look for plain statements where the cursor is at the end of the command.
|
2020-06-21 00:28:31 +00:00
|
|
|
const ast::string_t *matching_cmd_node = nullptr;
|
|
|
|
for (const node_t &n : ast) {
|
2020-08-27 20:28:17 +00:00
|
|
|
const auto *stmt = n.try_as<decorated_statement_t>();
|
2020-06-21 00:28:31 +00:00
|
|
|
if (!stmt) continue;
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2020-06-21 00:28:31 +00:00
|
|
|
// Skip if we have a decoration.
|
|
|
|
if (stmt->opt_decoration) continue;
|
2018-01-08 03:50:34 +00:00
|
|
|
|
2020-06-21 00:28:31 +00:00
|
|
|
// See if the command's source range range contains our cursor, including at the end.
|
|
|
|
auto msource = stmt->command.try_source_range();
|
2018-01-08 03:50:34 +00:00
|
|
|
if (!msource) continue;
|
2014-01-15 09:40:40 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Now see if its source range contains our cursor, including at the end.
|
2018-01-08 03:50:34 +00:00
|
|
|
if (subcmd_cursor_pos >= msource->start &&
|
|
|
|
subcmd_cursor_pos <= msource->start + msource->length) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Success!
|
2020-06-21 00:28:31 +00:00
|
|
|
matching_cmd_node = &stmt->command;
|
2013-07-17 07:38:04 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Now if we found a command node, expand it.
|
2020-02-04 11:47:44 +00:00
|
|
|
maybe_t<edit_t> result{};
|
2018-01-08 03:50:34 +00:00
|
|
|
if (matching_cmd_node) {
|
2020-06-21 00:28:31 +00:00
|
|
|
assert(!matching_cmd_node->unsourced && "Should not be unsourced");
|
|
|
|
const wcstring token = matching_cmd_node->source(subcmd);
|
2018-09-25 04:17:05 +00:00
|
|
|
if (auto abbreviation = expand_abbreviation(token, vars)) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// There was an abbreviation! Replace the token in the full command. Maintain the
|
|
|
|
// relative position of the cursor.
|
2020-06-21 00:28:31 +00:00
|
|
|
source_range_t r = matching_cmd_node->source_range();
|
2020-02-04 11:47:44 +00:00
|
|
|
result = edit_t(subcmd_offset + r.start, r.length, std::move(*abbreviation));
|
2013-07-17 07:38:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may
|
2016-05-03 17:36:22 +00:00
|
|
|
/// change the command line but does NOT repaint it. This is to allow the caller to coalesce
|
|
|
|
/// repaints.
|
2018-09-25 04:17:05 +00:00
|
|
|
bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
|
2013-07-17 07:38:04 +00:00
|
|
|
bool result = false;
|
2019-03-03 21:59:35 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2019-03-13 05:38:42 +00:00
|
|
|
|
2020-08-03 21:53:09 +00:00
|
|
|
if (conf.expand_abbrev_ok && el == &command_line) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Try expanding abbreviations.
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack);
|
2019-03-13 05:38:42 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
if (auto edit = reader_expand_abbreviation_in_command(el->text(), cursor_pos, vars())) {
|
2020-02-09 17:39:14 +00:00
|
|
|
push_edit(el, std::move(*edit));
|
2020-02-04 11:47:44 +00:00
|
|
|
update_buff_pos(el);
|
2013-07-17 07:38:04 +00:00
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2007-02-18 23:25:20 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
void reader_reset_interrupted() { interrupted = 0; }
|
2013-01-20 21:23:27 +00:00
|
|
|
|
2020-01-14 23:20:04 +00:00
|
|
|
int reader_test_and_clear_interrupted() {
|
2013-01-22 10:00:02 +00:00
|
|
|
int res = interrupted;
|
2016-05-03 17:36:22 +00:00
|
|
|
if (res) {
|
|
|
|
interrupted = 0;
|
2013-01-20 21:23:27 +00:00
|
|
|
}
|
2020-01-14 23:20:04 +00:00
|
|
|
return res;
|
2013-01-22 10:00:02 +00:00
|
|
|
}
|
|
|
|
|
2019-05-05 03:20:52 +00:00
|
|
|
void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor_position) {
|
2017-01-10 00:03:19 +00:00
|
|
|
if (!term_supports_setting_title()) return;
|
|
|
|
|
2019-05-27 21:52:48 +00:00
|
|
|
scoped_push<bool> noninteractive{&parser.libdata().is_interactive, false};
|
2019-10-19 01:08:22 +00:00
|
|
|
scoped_push<bool> in_title(&parser.libdata().suppress_fish_trace, true);
|
2019-05-27 21:52:48 +00:00
|
|
|
|
2021-02-28 07:40:39 +00:00
|
|
|
wcstring fish_title_command = DEFAULT_TITLE;
|
2019-05-05 03:20:52 +00:00
|
|
|
if (function_exists(L"fish_title", parser)) {
|
2014-08-03 04:01:40 +00:00
|
|
|
fish_title_command = L"fish_title";
|
2021-02-28 07:40:39 +00:00
|
|
|
if (!cmd.empty()) {
|
|
|
|
fish_title_command.append(L" ");
|
|
|
|
fish_title_command.append(
|
|
|
|
escape_string(cmd, ESCAPE_ALL | ESCAPE_NO_QUOTED | ESCAPE_NO_TILDE));
|
|
|
|
}
|
2014-07-14 15:34:15 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2011-12-28 20:36:47 +00:00
|
|
|
wcstring_list_t lst;
|
2020-01-24 01:34:46 +00:00
|
|
|
(void)exec_subshell(fish_title_command, parser, lst, false /* ignore exit status */);
|
|
|
|
if (!lst.empty()) {
|
2019-03-12 21:06:01 +00:00
|
|
|
std::fputws(L"\x1B]0;", stdout);
|
2019-11-19 21:46:47 +00:00
|
|
|
for (const auto &i : lst) {
|
|
|
|
std::fputws(i.c_str(), stdout);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2017-08-05 22:08:39 +00:00
|
|
|
ignore_result(write(STDOUT_FILENO, "\a", 1));
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2017-01-20 04:58:12 +00:00
|
|
|
|
2018-10-06 20:32:08 +00:00
|
|
|
outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset());
|
2016-05-03 17:36:22 +00:00
|
|
|
if (reset_cursor_position && !lst.empty()) {
|
|
|
|
// Put the cursor back at the beginning of the line (issue #2453).
|
2017-08-12 14:54:26 +00:00
|
|
|
ignore_result(write(STDOUT_FILENO, "\r", 1));
|
2016-04-29 19:14:10 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2019-04-01 13:52:21 +00:00
|
|
|
void reader_data_t::exec_mode_prompt() {
|
|
|
|
mode_prompt_buff.clear();
|
2019-05-05 03:20:52 +00:00
|
|
|
if (function_exists(MODE_PROMPT_FUNCTION_NAME, parser())) {
|
2019-04-01 13:52:21 +00:00
|
|
|
wcstring_list_t mode_indicator_list;
|
2019-05-05 10:09:25 +00:00
|
|
|
exec_subshell(MODE_PROMPT_FUNCTION_NAME, parser(), mode_indicator_list, false);
|
2019-04-01 13:52:21 +00:00
|
|
|
// We do not support multiple lines in the mode indicator, so just concatenate all of
|
|
|
|
// them.
|
2019-11-19 21:46:47 +00:00
|
|
|
for (const auto &i : mode_indicator_list) {
|
|
|
|
mode_prompt_buff += i;
|
2019-04-01 13:52:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-03 22:38:53 +00:00
|
|
|
/// Reexecute the prompt command. The output is inserted into prompt_buff.
|
|
|
|
void reader_data_t::exec_prompt() {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Clear existing prompts.
|
2019-03-03 22:38:53 +00:00
|
|
|
left_prompt_buff.clear();
|
|
|
|
right_prompt_buff.clear();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-10-19 01:08:22 +00:00
|
|
|
// Suppress fish_trace while in the prompt.
|
|
|
|
scoped_push<bool> in_prompt(&parser().libdata().suppress_fish_trace, true);
|
|
|
|
|
2020-06-08 02:35:47 +00:00
|
|
|
// Update the termsize now.
|
2018-03-07 16:42:56 +00:00
|
|
|
// This allows prompts to react to $COLUMNS.
|
2020-06-08 02:35:47 +00:00
|
|
|
update_termsize();
|
2018-03-07 16:42:56 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// If we have any prompts, they must be run non-interactively.
|
2020-08-03 21:53:09 +00:00
|
|
|
if (!conf.left_prompt_cmd.empty() || !conf.right_prompt_cmd.empty()) {
|
2019-05-27 21:52:48 +00:00
|
|
|
scoped_push<bool> noninteractive{&parser().libdata().is_interactive, false};
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-04-01 13:52:21 +00:00
|
|
|
exec_mode_prompt();
|
2015-06-04 20:31:48 +00:00
|
|
|
|
2020-08-03 21:53:09 +00:00
|
|
|
if (!conf.left_prompt_cmd.empty()) {
|
|
|
|
// Status is ignored.
|
2012-11-08 03:59:20 +00:00
|
|
|
wcstring_list_t prompt_list;
|
2020-08-03 21:53:09 +00:00
|
|
|
// Historic compatibility hack.
|
|
|
|
// If the left prompt function is deleted, then use a default prompt instead of
|
|
|
|
// producing an error.
|
|
|
|
bool left_prompt_deleted = conf.left_prompt_cmd == LEFT_PROMPT_FUNCTION_NAME &&
|
|
|
|
!function_exists(conf.left_prompt_cmd, parser());
|
|
|
|
exec_subshell(left_prompt_deleted ? DEFAULT_PROMPT : conf.left_prompt_cmd, parser(),
|
|
|
|
prompt_list, false);
|
|
|
|
left_prompt_buff = join_strings(prompt_list, L'\n');
|
2012-11-08 03:59:20 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-08-03 21:53:09 +00:00
|
|
|
if (!conf.right_prompt_cmd.empty()) {
|
2020-09-02 15:48:24 +00:00
|
|
|
if (function_exists(conf.right_prompt_cmd, parser())) {
|
|
|
|
// Status is ignored.
|
|
|
|
wcstring_list_t prompt_list;
|
|
|
|
exec_subshell(conf.right_prompt_cmd, parser(), prompt_list, false);
|
|
|
|
// Right prompt does not support multiple lines, so just concatenate all of them.
|
|
|
|
for (const auto &i : prompt_list) {
|
|
|
|
right_prompt_buff += i;
|
|
|
|
}
|
2012-11-08 03:59:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Write the screen title. Do not reset the cursor position: exec_prompt is called when there
|
|
|
|
// may still be output on the line from the previous command (#2499) and we need our PROMPT_SP
|
|
|
|
// hack to work.
|
2019-05-05 03:20:52 +00:00
|
|
|
reader_write_title(L"", parser(), false);
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
void reader_init() {
|
2020-06-08 02:35:47 +00:00
|
|
|
parser_t &parser = parser_t::principal_parser();
|
|
|
|
auto &vars = parser.vars();
|
2018-09-14 07:36:26 +00:00
|
|
|
|
2017-01-07 05:14:29 +00:00
|
|
|
// Ensure this var is present even before an interactive command is run so that if it is used
|
|
|
|
// in a function like `fish_prompt` or `fish_right_prompt` it is defined at the time the first
|
2017-08-11 00:46:08 +00:00
|
|
|
// prompt is written.
|
2018-09-14 07:36:26 +00:00
|
|
|
vars.set_one(ENV_CMD_DURATION, ENV_UNEXPORT, L"0");
|
2017-01-07 05:14:29 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Save the initial terminal mode.
|
2013-09-23 01:11:53 +00:00
|
|
|
tcgetattr(STDIN_FILENO, &terminal_mode_on_startup);
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Set the mode used for program execution, initialized to the current mode.
|
2019-03-12 22:07:07 +00:00
|
|
|
std::memcpy(&tty_modes_for_external_cmds, &terminal_mode_on_startup,
|
2019-05-05 10:09:25 +00:00
|
|
|
sizeof tty_modes_for_external_cmds);
|
2020-09-27 11:53:04 +00:00
|
|
|
term_fix_external_modes(&tty_modes_for_external_cmds);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Set the mode used for the terminal, initialized to the current mode.
|
2019-03-12 22:07:07 +00:00
|
|
|
std::memcpy(&shell_modes, &terminal_mode_on_startup, sizeof shell_modes);
|
2016-05-25 03:42:50 +00:00
|
|
|
|
2021-02-11 20:17:24 +00:00
|
|
|
// Disable flow control by default.
|
|
|
|
tty_modes_for_external_cmds.c_iflag &= ~IXON;
|
|
|
|
tty_modes_for_external_cmds.c_iflag &= ~IXOFF;
|
|
|
|
shell_modes.c_iflag &= ~IXON;
|
|
|
|
shell_modes.c_iflag &= ~IXOFF;
|
|
|
|
|
2020-05-16 10:39:34 +00:00
|
|
|
term_fix_modes(&shell_modes);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2021-03-06 08:24:54 +00:00
|
|
|
// Set up our fixed terminal modes once,
|
|
|
|
// so we don't get flow control just because we inherited it.
|
2021-03-10 08:38:16 +00:00
|
|
|
if (getpgrp() == tcgetpgrp(STDIN_FILENO)) {
|
|
|
|
term_donate(/* quiet */ true);
|
|
|
|
}
|
2021-03-06 08:24:54 +00:00
|
|
|
|
2017-06-18 22:38:52 +00:00
|
|
|
// We do this not because we actually need the window size but for its side-effect of correctly
|
|
|
|
// setting the COLUMNS and LINES env vars.
|
2020-06-08 02:35:47 +00:00
|
|
|
termsize_container_t::shared().updating(parser);
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2017-02-11 02:47:02 +00:00
|
|
|
/// Restore the term mode if we own the terminal. It's important we do this before
|
|
|
|
/// restore_foreground_process_group, otherwise we won't think we own the terminal.
|
2016-05-03 17:36:22 +00:00
|
|
|
void restore_term_mode() {
|
2019-02-25 19:23:34 +00:00
|
|
|
if (getpgrp() != tcgetpgrp(STDIN_FILENO)) return;
|
2017-02-11 02:47:02 +00:00
|
|
|
|
|
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &terminal_mode_on_startup) == -1 && errno == EIO) {
|
|
|
|
redirect_tty_output();
|
2013-10-26 22:22:20 +00:00
|
|
|
}
|
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Indicates if the given command char ends paging.
|
2019-03-17 00:56:35 +00:00
|
|
|
static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field) {
|
|
|
|
using rl = readline_cmd_t;
|
2016-05-03 17:36:22 +00:00
|
|
|
switch (c) {
|
2019-09-27 15:56:47 +00:00
|
|
|
case rl::history_prefix_search_backward:
|
|
|
|
case rl::history_prefix_search_forward:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::history_search_backward:
|
|
|
|
case rl::history_search_forward:
|
|
|
|
case rl::history_token_search_backward:
|
|
|
|
case rl::history_token_search_forward:
|
|
|
|
case rl::accept_autosuggestion:
|
2019-06-05 17:02:32 +00:00
|
|
|
case rl::delete_or_exit:
|
2020-07-01 18:54:13 +00:00
|
|
|
case rl::cancel_commandline:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::cancel: {
|
2016-05-03 17:36:22 +00:00
|
|
|
// These commands always end paging.
|
2014-01-27 08:56:13 +00:00
|
|
|
return true;
|
2016-05-03 17:36:22 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::complete:
|
2019-06-16 21:38:27 +00:00
|
|
|
case rl::complete_and_search:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_char:
|
|
|
|
case rl::forward_char:
|
2020-07-21 13:08:38 +00:00
|
|
|
case rl::forward_single_char:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::up_line:
|
|
|
|
case rl::down_line:
|
|
|
|
case rl::repaint:
|
|
|
|
case rl::suppress_autosuggestion:
|
|
|
|
case rl::beginning_of_history:
|
|
|
|
case rl::end_of_history: {
|
2016-11-03 01:29:14 +00:00
|
|
|
// These commands never end paging.
|
2014-01-27 08:56:13 +00:00
|
|
|
return false;
|
2016-05-03 17:36:22 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::execute: {
|
|
|
|
// execute does end paging, but only executes if it was not paging. So it's handled
|
2016-05-03 17:36:22 +00:00
|
|
|
// specially.
|
2014-01-28 06:01:38 +00:00
|
|
|
return false;
|
2016-05-03 17:36:22 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::beginning_of_line:
|
|
|
|
case rl::end_of_line:
|
|
|
|
case rl::forward_word:
|
|
|
|
case rl::backward_word:
|
|
|
|
case rl::forward_bigword:
|
|
|
|
case rl::backward_bigword:
|
|
|
|
case rl::delete_char:
|
|
|
|
case rl::backward_delete_char:
|
|
|
|
case rl::kill_line:
|
|
|
|
case rl::yank:
|
2019-09-21 21:31:13 +00:00
|
|
|
case rl::yank_pop:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_kill_line:
|
|
|
|
case rl::kill_whole_line:
|
|
|
|
case rl::kill_word:
|
|
|
|
case rl::kill_bigword:
|
|
|
|
case rl::backward_kill_word:
|
|
|
|
case rl::backward_kill_path_component:
|
|
|
|
case rl::backward_kill_bigword:
|
|
|
|
case rl::self_insert:
|
2020-03-04 22:10:24 +00:00
|
|
|
case rl::self_insert_notfirst:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::transpose_chars:
|
|
|
|
case rl::transpose_words:
|
|
|
|
case rl::upcase_word:
|
|
|
|
case rl::downcase_word:
|
|
|
|
case rl::capitalize_word:
|
|
|
|
case rl::beginning_of_buffer:
|
|
|
|
case rl::end_of_buffer:
|
2020-02-04 11:47:44 +00:00
|
|
|
case rl::undo:
|
|
|
|
case rl::redo:
|
2016-05-03 17:36:22 +00:00
|
|
|
// These commands operate on the search field if that's where the focus is.
|
|
|
|
return !focused_on_search_field;
|
2019-03-17 00:56:35 +00:00
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Indicates if the given command ends the history search.
|
|
|
|
static bool command_ends_history_search(readline_cmd_t c) {
|
|
|
|
switch (c) {
|
2019-09-27 15:56:47 +00:00
|
|
|
case readline_cmd_t::history_prefix_search_backward:
|
|
|
|
case readline_cmd_t::history_prefix_search_forward:
|
2019-03-24 00:32:39 +00:00
|
|
|
case readline_cmd_t::history_search_backward:
|
|
|
|
case readline_cmd_t::history_search_forward:
|
|
|
|
case readline_cmd_t::history_token_search_backward:
|
|
|
|
case readline_cmd_t::history_token_search_forward:
|
2020-02-09 17:39:14 +00:00
|
|
|
case readline_cmd_t::beginning_of_history:
|
|
|
|
case readline_cmd_t::end_of_history:
|
2019-03-24 00:32:39 +00:00
|
|
|
case readline_cmd_t::repaint:
|
|
|
|
case readline_cmd_t::force_repaint:
|
2019-03-17 00:56:35 +00:00
|
|
|
return false;
|
|
|
|
default:
|
|
|
|
return true;
|
2014-01-27 08:56:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Remove the previous character in the character buffer and on the screen using syntax
|
|
|
|
/// highlighting, etc.
|
2020-02-04 11:47:44 +00:00
|
|
|
void reader_data_t::delete_char(bool backward) {
|
2019-03-03 23:00:29 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t pos = el->position();
|
|
|
|
if (!backward) {
|
|
|
|
pos++;
|
|
|
|
}
|
|
|
|
size_t pos_end = pos;
|
|
|
|
|
2020-02-09 09:23:30 +00:00
|
|
|
if (el->position() == 0 && backward) return;
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Fake composed character sequences by continuing to delete until we delete a character of
|
|
|
|
// width at least 1.
|
2012-07-15 17:45:18 +00:00
|
|
|
int width;
|
2016-05-03 17:36:22 +00:00
|
|
|
do {
|
2020-02-04 11:47:44 +00:00
|
|
|
pos--;
|
|
|
|
width = fish_wcwidth(el->text().at(pos));
|
|
|
|
} while (width == 0 && pos > 0);
|
2020-02-09 17:39:14 +00:00
|
|
|
erase_substring(el, pos, pos_end - pos);
|
2020-02-04 11:47:44 +00:00
|
|
|
update_buff_pos(el);
|
2019-03-03 23:00:29 +00:00
|
|
|
suppress_autosuggestion = true;
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Insert the characters of the string into the command line buffer and print them to the screen
|
2019-04-01 13:59:15 +00:00
|
|
|
/// using syntax highlighting, etc.
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Returns true if the string changed.
|
2020-08-23 22:12:47 +00:00
|
|
|
void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) {
|
2020-02-09 17:39:14 +00:00
|
|
|
if (str.empty()) return;
|
|
|
|
|
|
|
|
command_line_has_transient_edit = false;
|
|
|
|
if (!history_search.active() && want_to_coalesce_insertion_of(*el, str)) {
|
|
|
|
el->insert_coalesce(str);
|
|
|
|
assert(el->undo_history.may_coalesce);
|
|
|
|
} else {
|
|
|
|
el->push_edit(edit_t(el->position(), 0, str));
|
2021-01-05 21:40:09 +00:00
|
|
|
el->undo_history.may_coalesce = el->undo_history.try_coalesce || (str.size() == 1);
|
2020-02-09 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (el == &command_line) suppress_autosuggestion = false;
|
|
|
|
// The pager needs to be refiltered.
|
|
|
|
if (el == &this->pager.search_field_line) {
|
|
|
|
command_line_changed(el);
|
2014-01-26 22:47:15 +00:00
|
|
|
}
|
2013-08-24 18:16:45 +00:00
|
|
|
}
|
2006-11-02 13:50:19 +00:00
|
|
|
|
2020-02-09 17:39:14 +00:00
|
|
|
void reader_data_t::push_edit(editable_line_t *el, edit_t &&edit) {
|
|
|
|
el->push_edit(std::move(edit));
|
|
|
|
el->undo_history.may_coalesce = false;
|
|
|
|
// The pager needs to be refiltered.
|
|
|
|
if (el == &this->pager.search_field_line) {
|
|
|
|
command_line_changed(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::erase_substring(editable_line_t *el, size_t offset, size_t length) {
|
|
|
|
push_edit(el, edit_t(offset, length, L""));
|
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::replace_substring(editable_line_t *el, size_t offset, size_t length,
|
2020-12-24 10:00:20 +00:00
|
|
|
wcstring replacement) {
|
|
|
|
push_edit(el, edit_t(offset, length, std::move(replacement)));
|
2020-02-09 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Insert the string in the given command line at the given cursor position. The function checks if
|
|
|
|
/// the string is quoted or not and correctly escapes the string.
|
|
|
|
///
|
|
|
|
/// \param val the string to insert
|
|
|
|
/// \param flags A union of all flags describing the completion to insert. See the completion_t
|
|
|
|
/// struct for more information on possible values.
|
|
|
|
/// \param command_line The command line into which we will insert
|
|
|
|
/// \param inout_cursor_pos On input, the location of the cursor within the command line. On output,
|
|
|
|
/// the new desired position.
|
|
|
|
/// \param append_only Whether we can only append to the command line, or also modify previous
|
|
|
|
/// characters. This is used to determine whether we go inside a trailing quote.
|
|
|
|
///
|
|
|
|
/// \return The completed string
|
2018-10-20 20:35:04 +00:00
|
|
|
wcstring completion_apply_to_command_line(const wcstring &val, complete_flags_t flags,
|
2016-05-03 17:36:22 +00:00
|
|
|
const wcstring &command_line, size_t *inout_cursor_pos,
|
|
|
|
bool append_only) {
|
2018-02-17 22:36:43 +00:00
|
|
|
bool add_space = !bool(flags & COMPLETE_NO_SPACE);
|
|
|
|
bool do_replace = bool(flags & COMPLETE_REPLACES_TOKEN);
|
|
|
|
bool do_escape = !bool(flags & COMPLETE_DONT_ESCAPE);
|
|
|
|
bool no_tilde = bool(flags & COMPLETE_DONT_ESCAPE_TILDES);
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2012-05-05 20:34:09 +00:00
|
|
|
const size_t cursor_pos = *inout_cursor_pos;
|
2013-02-02 22:50:22 +00:00
|
|
|
bool back_into_trailing_quote = false;
|
2019-09-10 18:42:41 +00:00
|
|
|
bool have_space_after_token = command_line[cursor_pos] == L' ';
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (do_replace) {
|
2012-11-19 00:30:30 +00:00
|
|
|
size_t move_cursor;
|
|
|
|
const wchar_t *begin, *end;
|
|
|
|
|
2012-05-05 20:34:09 +00:00
|
|
|
const wchar_t *buff = command_line.c_str();
|
2019-11-19 02:34:50 +00:00
|
|
|
parse_util_token_extent(buff, cursor_pos, &begin, nullptr, nullptr, nullptr);
|
2012-11-19 00:30:30 +00:00
|
|
|
end = buff + cursor_pos;
|
|
|
|
|
|
|
|
wcstring sb(buff, begin - buff);
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (do_escape) {
|
2017-03-14 03:19:08 +00:00
|
|
|
wcstring escaped = escape_string(
|
|
|
|
val, ESCAPE_ALL | ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0));
|
2012-11-19 00:30:30 +00:00
|
|
|
sb.append(escaped);
|
2014-09-26 01:20:03 +00:00
|
|
|
move_cursor = escaped.size();
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2012-11-19 00:30:30 +00:00
|
|
|
sb.append(val);
|
2018-10-20 20:35:04 +00:00
|
|
|
move_cursor = val.length();
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (add_space) {
|
2019-09-10 18:42:41 +00:00
|
|
|
if (!have_space_after_token) sb.append(L" ");
|
2012-11-19 00:30:30 +00:00
|
|
|
move_cursor += 1;
|
|
|
|
}
|
|
|
|
sb.append(end);
|
|
|
|
|
2012-05-05 20:34:09 +00:00
|
|
|
size_t new_cursor_pos = (begin - buff) + move_cursor;
|
|
|
|
*inout_cursor_pos = new_cursor_pos;
|
|
|
|
return sb;
|
2016-05-04 22:19:47 +00:00
|
|
|
}
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2016-05-04 22:19:47 +00:00
|
|
|
wchar_t quote = L'\0';
|
|
|
|
wcstring replaced;
|
|
|
|
if (do_escape) {
|
2019-09-12 15:58:42 +00:00
|
|
|
// We need to figure out whether the token we complete has unclosed quotes. Since the token
|
|
|
|
// may be inside a command substitutions we must first determine the extents of the
|
|
|
|
// innermost command substitution.
|
|
|
|
const wchar_t *cmdsub_begin, *cmdsub_end;
|
|
|
|
parse_util_cmdsubst_extent(command_line.c_str(), cursor_pos, &cmdsub_begin, &cmdsub_end);
|
|
|
|
size_t cmdsub_offset = cmdsub_begin - command_line.c_str();
|
|
|
|
// Find the last quote in the token to complete. By parsing only the string inside any
|
|
|
|
// command substitution, we prevent the tokenizer from treating the entire command
|
|
|
|
// substitution as one token.
|
2019-10-13 22:50:48 +00:00
|
|
|
parse_util_get_parameter_info(
|
|
|
|
command_line.substr(cmdsub_offset, (cmdsub_end - cmdsub_begin)),
|
2019-11-19 02:34:50 +00:00
|
|
|
cursor_pos - cmdsub_offset, "e, nullptr, nullptr);
|
2016-05-04 22:19:47 +00:00
|
|
|
|
|
|
|
// If the token is reported as unquoted, but ends with a (unescaped) quote, and we can
|
|
|
|
// modify the command line, then delete the trailing quote so that we can insert within
|
|
|
|
// the quotes instead of after them. See issue #552.
|
|
|
|
if (quote == L'\0' && !append_only && cursor_pos > 0) {
|
|
|
|
// The entire token is reported as unquoted...see if the last character is an
|
|
|
|
// unescaped quote.
|
|
|
|
wchar_t trailing_quote = unescaped_quote(command_line, cursor_pos - 1);
|
|
|
|
if (trailing_quote != L'\0') {
|
|
|
|
quote = trailing_quote;
|
|
|
|
back_into_trailing_quote = true;
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2018-10-20 20:35:04 +00:00
|
|
|
replaced = parse_util_escape_string_with_quote(val, quote, no_tilde);
|
2016-05-04 22:19:47 +00:00
|
|
|
} else {
|
|
|
|
replaced = val;
|
|
|
|
}
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2016-05-04 22:19:47 +00:00
|
|
|
size_t insertion_point = cursor_pos;
|
|
|
|
if (back_into_trailing_quote) {
|
|
|
|
// Move the character back one so we enter the terminal quote.
|
|
|
|
assert(insertion_point > 0);
|
|
|
|
insertion_point--;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the insertion and compute the new location.
|
|
|
|
wcstring result = command_line;
|
|
|
|
result.insert(insertion_point, replaced);
|
2016-05-25 03:42:50 +00:00
|
|
|
size_t new_cursor_pos = insertion_point + replaced.size() + (back_into_trailing_quote ? 1 : 0);
|
2016-05-04 22:19:47 +00:00
|
|
|
if (add_space) {
|
|
|
|
if (quote != L'\0' && unescaped_quote(command_line, insertion_point) != quote) {
|
|
|
|
// This is a quoted parameter, first print a quote.
|
|
|
|
result.insert(new_cursor_pos++, wcstring("e, 1));
|
2012-05-05 20:34:09 +00:00
|
|
|
}
|
2019-09-10 18:42:41 +00:00
|
|
|
if (!have_space_after_token) result.insert(new_cursor_pos, L" ");
|
|
|
|
new_cursor_pos++;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2016-05-04 22:19:47 +00:00
|
|
|
*inout_cursor_pos = new_cursor_pos;
|
|
|
|
return result;
|
2012-05-05 20:34:09 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Insert the string at the current cursor position. The function checks if the string is quoted or
|
|
|
|
/// not and correctly escapes the string.
|
|
|
|
///
|
|
|
|
/// \param val the string to insert
|
2019-09-09 19:54:44 +00:00
|
|
|
/// \param token_end the position after the token to complete
|
2016-05-03 17:36:22 +00:00
|
|
|
/// \param flags A union of all flags describing the completion to insert. See the completion_t
|
|
|
|
/// struct for more information on possible values.
|
2020-11-15 14:24:37 +00:00
|
|
|
void reader_data_t::completion_insert(const wcstring &val, size_t token_end,
|
2019-09-09 19:54:44 +00:00
|
|
|
complete_flags_t flags) {
|
2019-03-03 23:00:29 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2019-09-09 19:54:44 +00:00
|
|
|
|
|
|
|
// Move the cursor to the end of the token.
|
2020-08-23 22:12:47 +00:00
|
|
|
if (el->position() != token_end) update_buff_pos(el, token_end);
|
2019-09-09 19:54:44 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t cursor = el->position();
|
|
|
|
wcstring new_command_line = completion_apply_to_command_line(val, flags, el->text(), &cursor,
|
2016-05-03 17:36:22 +00:00
|
|
|
false /* not append only */);
|
2019-03-03 23:00:29 +00:00
|
|
|
set_buffer_maintaining_pager(new_command_line, cursor);
|
2012-05-05 20:34:09 +00:00
|
|
|
}
|
|
|
|
|
2017-01-24 17:27:11 +00:00
|
|
|
// Returns a function that can be invoked (potentially
|
|
|
|
// on a background thread) to determine the autosuggestion
|
2020-11-27 22:41:35 +00:00
|
|
|
static std::function<autosuggestion_t(void)> get_autosuggestion_performer(
|
2021-01-10 00:22:32 +00:00
|
|
|
parser_t &parser, const wcstring &search_string, size_t cursor_pos,
|
|
|
|
const std::shared_ptr<history_t> &history) {
|
2020-08-22 19:30:33 +00:00
|
|
|
const uint32_t generation_count = read_generation_count();
|
2019-05-27 01:51:26 +00:00
|
|
|
auto vars = parser.vars().snapshot();
|
2019-04-14 22:38:58 +00:00
|
|
|
const wcstring working_directory = vars->get_pwd_slash();
|
2017-01-24 17:27:11 +00:00
|
|
|
// TODO: suspicious use of 'history' here
|
|
|
|
// This is safe because histories are immortal, but perhaps
|
|
|
|
// this should use shared_ptr
|
2020-11-27 22:41:35 +00:00
|
|
|
return [=]() -> autosuggestion_t {
|
2017-01-25 01:52:28 +00:00
|
|
|
ASSERT_IS_BACKGROUND_THREAD();
|
2020-11-27 22:41:35 +00:00
|
|
|
autosuggestion_t nothing = {};
|
2020-01-16 01:14:47 +00:00
|
|
|
operation_context_t ctx = get_bg_context(vars, generation_count);
|
|
|
|
if (ctx.check_cancel()) {
|
2017-01-24 17:27:11 +00:00
|
|
|
return nothing;
|
2012-02-28 20:40:20 +00:00
|
|
|
}
|
2013-01-22 23:19:29 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Let's make sure we aren't using the empty string.
|
|
|
|
if (search_string.empty()) {
|
2017-01-24 17:27:11 +00:00
|
|
|
return nothing;
|
2012-05-05 20:34:09 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2021-01-02 22:34:19 +00:00
|
|
|
// Search history for a matching item.
|
2021-01-10 00:22:32 +00:00
|
|
|
history_search_t searcher(history.get(), search_string, history_search_type_t::prefix,
|
2021-01-02 22:34:19 +00:00
|
|
|
history_search_flags_t{});
|
|
|
|
while (!ctx.check_cancel() && searcher.go_backwards()) {
|
|
|
|
const history_item_t &item = searcher.current_item();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2021-01-02 22:34:19 +00:00
|
|
|
// Skip items with newlines because they make terrible autosuggestions.
|
|
|
|
if (item.str().find(L'\n') != wcstring::npos) continue;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2021-01-02 22:34:19 +00:00
|
|
|
if (autosuggest_validate_from_history(item, working_directory, ctx)) {
|
|
|
|
// The command autosuggestion was handled specially, so we're done.
|
|
|
|
// History items are case-sensitive, see #3978.
|
|
|
|
return autosuggestion_t{searcher.current_string(), search_string,
|
|
|
|
false /* icase */};
|
2012-02-20 10:13:31 +00:00
|
|
|
}
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Maybe cancel here.
|
2020-01-16 01:14:47 +00:00
|
|
|
if (ctx.check_cancel()) return nothing;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Here we do something a little funny. If the line ends with a space, and the cursor is not
|
|
|
|
// at the end, don't use completion autosuggestions. It ends up being pretty weird seeing
|
|
|
|
// stuff get spammed on the right while you go back to edit a line
|
2012-07-06 21:34:53 +00:00
|
|
|
const wchar_t last_char = search_string.at(search_string.size() - 1);
|
2017-01-24 17:27:11 +00:00
|
|
|
const bool cursor_at_end = (cursor_pos == search_string.size());
|
|
|
|
if (!cursor_at_end && iswspace(last_char)) return nothing;
|
2012-07-06 21:34:53 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// On the other hand, if the line ends with a quote, don't go dumping stuff after the quote.
|
2019-03-12 21:06:01 +00:00
|
|
|
if (std::wcschr(L"'\"", last_char) && cursor_at_end) return nothing;
|
2012-05-05 20:34:09 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Try normal completions.
|
2019-05-05 00:55:57 +00:00
|
|
|
completion_request_flags_t complete_flags = completion_request_t::autosuggestion;
|
2020-01-16 01:14:47 +00:00
|
|
|
completion_list_t completions = complete(search_string, complete_flags, ctx);
|
2018-08-07 09:01:19 +00:00
|
|
|
completions_sort_and_prioritize(&completions, complete_flags);
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!completions.empty()) {
|
2012-03-02 01:31:45 +00:00
|
|
|
const completion_t &comp = completions.at(0);
|
2017-01-24 17:27:11 +00:00
|
|
|
size_t cursor = cursor_pos;
|
2017-01-27 04:00:43 +00:00
|
|
|
wcstring suggestion = completion_apply_to_command_line(
|
|
|
|
comp.completion, comp.flags, search_string, &cursor, true /* append only */);
|
2020-11-27 23:00:16 +00:00
|
|
|
// Normal completions are case-insensitive.
|
|
|
|
return autosuggestion_t{std::move(suggestion), search_string, true /* icase */};
|
2012-02-19 02:54:36 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2017-01-24 17:27:11 +00:00
|
|
|
return nothing;
|
|
|
|
};
|
2012-02-16 08:24:27 +00:00
|
|
|
}
|
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
bool reader_data_t::can_autosuggest() const {
|
2016-05-03 17:36:22 +00:00
|
|
|
// We autosuggest if suppress_autosuggestion is not set, if we're not doing a history search,
|
|
|
|
// and our command line contains a non-whitespace character.
|
2019-03-03 23:00:29 +00:00
|
|
|
const editable_line_t *el = active_edit_line();
|
2012-03-05 17:34:31 +00:00
|
|
|
const wchar_t *whitespace = L" \t\r\n\v";
|
2020-08-03 21:53:09 +00:00
|
|
|
return conf.autosuggest_ok && !suppress_autosuggestion && history_search.is_at_end() &&
|
2020-02-04 11:47:44 +00:00
|
|
|
el == &command_line && el->text().find_first_not_of(whitespace) != wcstring::npos;
|
2012-02-16 08:24:27 +00:00
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Called after an autosuggestion has been computed on a background thread.
|
2020-11-27 22:41:35 +00:00
|
|
|
void reader_data_t::autosuggest_completed(autosuggestion_t result) {
|
2020-08-23 22:12:47 +00:00
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
if (result.search_string == in_flight_autosuggest_request)
|
|
|
|
in_flight_autosuggest_request.clear();
|
2020-11-27 22:41:35 +00:00
|
|
|
if (!result.empty() && can_autosuggest() && result.search_string == command_line.text() &&
|
|
|
|
string_prefixes_string_case_insensitive(result.search_string, result.text)) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Autosuggestion is active and the search term has not changed, so we're good to go.
|
2020-11-27 22:41:35 +00:00
|
|
|
autosuggestion = std::move(result);
|
2020-08-23 22:12:47 +00:00
|
|
|
if (this->is_repaint_needed()) {
|
|
|
|
this->layout_and_repaint(L"autosuggest");
|
|
|
|
}
|
2012-02-16 08:24:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-03 22:14:08 +00:00
|
|
|
void reader_data_t::update_autosuggestion() {
|
2020-08-23 22:12:47 +00:00
|
|
|
// If we can't autosuggest, just clear it.
|
|
|
|
if (!can_autosuggest()) {
|
|
|
|
in_flight_autosuggest_request.clear();
|
|
|
|
autosuggestion.clear();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check to see if our autosuggestion still applies; if so, don't recompute it.
|
|
|
|
// Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into
|
|
|
|
// the autosuggestion.
|
|
|
|
// This is also the main mechanism by which readline commands that don't change the command line
|
|
|
|
// text avoid recomputing the autosuggestion.
|
|
|
|
const editable_line_t &el = command_line;
|
|
|
|
if (!autosuggestion.empty() &&
|
2020-11-27 23:00:16 +00:00
|
|
|
(autosuggestion.icase
|
|
|
|
? string_prefixes_string_case_insensitive(el.text(), autosuggestion.text)
|
|
|
|
: string_prefixes_string(el.text(), autosuggestion.text))) {
|
2020-08-23 22:12:47 +00:00
|
|
|
return;
|
2020-08-23 11:07:49 +00:00
|
|
|
}
|
2020-08-23 22:12:47 +00:00
|
|
|
|
|
|
|
// Do nothing if we've already kicked off this autosuggest request.
|
|
|
|
if (el.text() == in_flight_autosuggest_request) return;
|
|
|
|
in_flight_autosuggest_request = el.text();
|
|
|
|
|
|
|
|
// Clear the autosuggestion and kick it off in the background.
|
|
|
|
FLOG(reader_render, L"Autosuggesting");
|
|
|
|
autosuggestion.clear();
|
|
|
|
auto performer = get_autosuggestion_performer(parser(), el.text(), el.position(), history);
|
|
|
|
auto shared_this = this->shared_from_this();
|
2020-11-27 22:41:35 +00:00
|
|
|
debounce_autosuggestions().perform(performer, [shared_this](autosuggestion_t result) {
|
2020-08-23 22:12:47 +00:00
|
|
|
shared_this->autosuggest_completed(std::move(result));
|
|
|
|
});
|
2012-02-06 18:52:13 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Accept any autosuggestion by replacing the command line with it. If full is true, take the whole
|
2019-02-19 21:25:14 +00:00
|
|
|
// thing; if it's false, then respect the passed in style.
|
2020-07-21 13:08:38 +00:00
|
|
|
void reader_data_t::accept_autosuggestion(bool full, bool single, move_word_style_t style) {
|
2019-03-03 22:11:09 +00:00
|
|
|
if (!autosuggestion.empty()) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Accepting an autosuggestion clears the pager.
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2015-09-19 03:47:38 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Accept the autosuggestion.
|
|
|
|
if (full) {
|
|
|
|
// Just take the whole thing.
|
2020-11-27 22:41:35 +00:00
|
|
|
replace_substring(&command_line, 0, command_line.size(), autosuggestion.text);
|
2020-07-21 13:08:38 +00:00
|
|
|
} else if (single) {
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(&command_line, command_line.size(), 0,
|
2020-11-27 22:41:35 +00:00
|
|
|
autosuggestion.text.substr(command_line.size(), 1));
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-02-19 21:25:14 +00:00
|
|
|
// Accept characters according to the specified style.
|
|
|
|
move_word_state_machine_t state(style);
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t want;
|
2020-11-27 22:41:35 +00:00
|
|
|
for (want = command_line.size(); want < autosuggestion.text.size(); want++) {
|
|
|
|
wchar_t wc = autosuggestion.text.at(want);
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!state.consume_char(wc)) break;
|
2012-12-11 00:23:08 +00:00
|
|
|
}
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t have = command_line.size();
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(&command_line, command_line.size(), 0,
|
2020-11-27 22:41:35 +00:00
|
|
|
autosuggestion.text.substr(have, want - have));
|
2012-12-11 00:23:08 +00:00
|
|
|
}
|
2012-07-15 21:02:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-28 21:06:03 +00:00
|
|
|
void reader_data_t::select_completion_in_direction(selection_motion_t dir) {
|
2019-03-03 23:47:00 +00:00
|
|
|
bool selection_changed = pager.select_next_completion_in_direction(dir, current_page_rendering);
|
2016-05-03 17:36:22 +00:00
|
|
|
if (selection_changed) {
|
2019-03-03 23:47:00 +00:00
|
|
|
pager_selection_changed();
|
2014-02-17 03:56:13 +00:00
|
|
|
}
|
2014-01-18 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Flash the screen. This function changes the color of the current line momentarily and sends a
|
|
|
|
/// BEL to maybe flash the screen or emite a sound, depending on how it is configured.
|
2019-03-03 23:47:00 +00:00
|
|
|
void reader_data_t::flash() {
|
2012-11-19 00:30:30 +00:00
|
|
|
struct timespec pollint;
|
2019-03-03 23:47:00 +00:00
|
|
|
editable_line_t *el = &command_line;
|
2020-08-23 22:12:47 +00:00
|
|
|
layout_data_t data = make_layout_data();
|
|
|
|
|
|
|
|
// Save off the colors and set the background.
|
|
|
|
highlight_list_t saved_colors = data.colors;
|
2020-02-04 11:47:44 +00:00
|
|
|
for (size_t i = 0; i < el->position(); i++) {
|
2020-08-23 22:12:47 +00:00
|
|
|
data.colors.at(i) = highlight_spec_t::make_background(highlight_role_t::search_match);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2020-08-23 22:12:47 +00:00
|
|
|
this->rendered_layout = data; // need to copy the data since we will use it again.
|
|
|
|
paint_layout(L"flash");
|
|
|
|
|
|
|
|
layout_data_t old_data = std::move(rendered_layout);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2017-08-12 14:54:26 +00:00
|
|
|
ignore_result(write(STDOUT_FILENO, "\a", 1));
|
2018-09-16 23:25:49 +00:00
|
|
|
// The write above changed the timestamp of stdout; ensure we don't therefore reset our screen.
|
|
|
|
// See #3693.
|
2019-03-03 23:47:00 +00:00
|
|
|
s_save_status(&screen);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
|
|
|
pollint.tv_sec = 0;
|
|
|
|
pollint.tv_nsec = 100 * 1000000;
|
2019-11-19 02:34:50 +00:00
|
|
|
nanosleep(&pollint, nullptr);
|
2006-11-30 23:57:49 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Re-render with our saved data.
|
|
|
|
data.colors = std::move(saved_colors);
|
|
|
|
this->rendered_layout = std::move(data);
|
|
|
|
paint_layout(L"unflash");
|
2006-11-30 23:57:49 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Characters that may not be part of a token that is to be replaced by a case insensitive
|
|
|
|
/// completion.
|
2008-01-14 01:00:32 +00:00
|
|
|
#define REPLACE_UNCLEAN L"$*?({})"
|
2008-01-13 19:32:21 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Check if the specified string can be replaced by a case insensitive completion with the
|
|
|
|
/// specified flags.
|
|
|
|
///
|
|
|
|
/// Advanced tokens like those containing {}-style expansion can not at the moment be replaced,
|
|
|
|
/// other than if the new token is already an exact replacement, e.g. if the COMPLETE_DONT_ESCAPE
|
|
|
|
/// flag is set.
|
|
|
|
static bool reader_can_replace(const wcstring &in, int flags) {
|
|
|
|
const wchar_t *str = in.c_str();
|
2008-01-13 19:32:21 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (flags & COMPLETE_DONT_ESCAPE) {
|
2012-11-19 00:30:30 +00:00
|
|
|
return true;
|
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
|
|
|
|
// Test characters that have a special meaning in any character position.
|
|
|
|
while (*str) {
|
2019-03-12 21:06:01 +00:00
|
|
|
if (std::wcschr(REPLACE_UNCLEAN, *str)) return false;
|
2012-11-19 00:30:30 +00:00
|
|
|
str++;
|
|
|
|
}
|
2008-01-13 19:32:21 +00:00
|
|
|
|
2012-11-19 00:30:30 +00:00
|
|
|
return true;
|
2012-08-22 00:18:52 +00:00
|
|
|
}
|
|
|
|
|
2020-11-28 21:39:36 +00:00
|
|
|
/// Determine the best (lowest) match rank for a set of completions.
|
|
|
|
static uint32_t get_best_rank(const completion_list_t &comp) {
|
|
|
|
uint32_t best_rank = UINT32_MAX;
|
|
|
|
for (const auto &c : comp) {
|
|
|
|
best_rank = std::min(best_rank, c.rank());
|
|
|
|
}
|
|
|
|
return best_rank;
|
2013-08-25 09:10:22 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Handle the list of completions. This means the following:
|
|
|
|
///
|
|
|
|
/// - If the list is empty, flash the terminal.
|
|
|
|
/// - If the list contains one element, write the whole element, and if the element does not end on
|
2021-01-25 17:44:33 +00:00
|
|
|
/// a '/', '@', ':', '.', ',', '-' or a '=', also write a trailing space.
|
2020-04-21 19:41:21 +00:00
|
|
|
/// - If the list contains multiple elements, insert their common prefix, if any and display
|
|
|
|
/// the list in the pager. Depending on terminal size and the length of the list, the pager
|
|
|
|
/// may either show less than a screenfull and exit or use an interactive pager to allow the
|
|
|
|
/// user to scroll through the completions.
|
2016-05-03 17:36:22 +00:00
|
|
|
///
|
|
|
|
/// \param comp the list of completion strings
|
2019-09-09 19:54:44 +00:00
|
|
|
/// \param token_begin the position of the token to complete
|
|
|
|
/// \param token_end the position after the token to complete
|
2016-05-03 17:36:22 +00:00
|
|
|
///
|
|
|
|
/// Return true if we inserted text into the command line, false if we did not.
|
2020-01-16 00:13:41 +00:00
|
|
|
bool reader_data_t::handle_completions(const completion_list_t &comp, size_t token_begin,
|
2020-04-21 19:41:21 +00:00
|
|
|
size_t token_end) {
|
2012-11-19 00:30:30 +00:00
|
|
|
bool done = false;
|
2012-08-22 00:18:52 +00:00
|
|
|
bool success = false;
|
2019-03-03 23:00:29 +00:00
|
|
|
const editable_line_t *el = &command_line;
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
const wcstring tok(el->text().c_str() + token_begin, token_end - token_begin);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Check trivial cases.
|
2016-11-19 23:45:08 +00:00
|
|
|
size_t size = comp.size();
|
2016-10-23 03:32:25 +00:00
|
|
|
if (size == 0) {
|
|
|
|
// No suitable completions found, flash screen and return.
|
2019-03-03 23:47:00 +00:00
|
|
|
flash();
|
2016-10-23 03:32:25 +00:00
|
|
|
done = true;
|
|
|
|
} else if (size == 1) {
|
|
|
|
// Exactly one suitable completion found - insert it.
|
|
|
|
const completion_t &c = comp.at(0);
|
|
|
|
|
|
|
|
// If this is a replacement completion, check that we know how to replace it, e.g. that
|
|
|
|
// the token doesn't contain evil operators like {}.
|
|
|
|
if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags)) {
|
2020-11-15 14:24:37 +00:00
|
|
|
completion_insert(c.completion, token_end, c.flags);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2016-10-23 03:32:25 +00:00
|
|
|
done = true;
|
|
|
|
success = true;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-10-30 21:16:32 +00:00
|
|
|
if (done) {
|
|
|
|
return success;
|
|
|
|
}
|
2013-06-02 08:14:26 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
auto best_rank = get_best_rank(comp);
|
|
|
|
|
|
|
|
// Determine whether we are going to replace the token or not. If any commands of the best
|
|
|
|
// rank do not require replacement, then ignore all those that want to use replacement.
|
|
|
|
bool will_replace_token = true;
|
|
|
|
for (const completion_t &el : comp) {
|
|
|
|
if (el.rank() <= best_rank && !(el.flags & COMPLETE_REPLACES_TOKEN)) {
|
|
|
|
will_replace_token = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-30 21:16:32 +00:00
|
|
|
// Decide which completions survived. There may be a lot of them; it would be nice if we could
|
|
|
|
// figure out how to avoid copying them here.
|
2020-01-16 00:13:41 +00:00
|
|
|
completion_list_t surviving_completions;
|
2020-11-28 21:39:36 +00:00
|
|
|
bool all_matches_exact_or_prefix = true;
|
2020-12-02 18:11:36 +00:00
|
|
|
for (const completion_t &el : comp) {
|
2020-11-28 21:39:36 +00:00
|
|
|
// Ignore completions with a less suitable match rank than the best.
|
2020-12-02 18:11:36 +00:00
|
|
|
if (el.rank() > best_rank) continue;
|
|
|
|
|
|
|
|
// Only use completions that match replace_token.
|
|
|
|
bool completion_replace_token = static_cast<bool>(el.flags & COMPLETE_REPLACES_TOKEN);
|
|
|
|
if (completion_replace_token != will_replace_token) continue;
|
2016-10-30 21:16:32 +00:00
|
|
|
|
|
|
|
// Don't use completions that want to replace, if we cannot replace them.
|
2020-12-02 18:11:36 +00:00
|
|
|
if (completion_replace_token && !reader_can_replace(tok, el.flags)) continue;
|
2016-10-30 21:16:32 +00:00
|
|
|
|
|
|
|
// This completion survived.
|
2020-12-02 18:11:36 +00:00
|
|
|
surviving_completions.push_back(el);
|
|
|
|
all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match.is_exact_or_prefix();
|
|
|
|
}
|
|
|
|
|
2021-02-22 21:48:07 +00:00
|
|
|
if (surviving_completions.size() == 1) {
|
|
|
|
// After sorting and stuff only one completion is left, use it.
|
|
|
|
//
|
|
|
|
// TODO: This happens when smartcase kicks in, e.g.
|
|
|
|
// the token is "cma" and the options are "cmake/" and "CMakeLists.txt"
|
|
|
|
// it would be nice if we could figure
|
|
|
|
// out how to use it more.
|
|
|
|
const completion_t &c = surviving_completions.at(0);
|
|
|
|
|
|
|
|
// If this is a replacement completion, check that we know how to replace it, e.g. that
|
|
|
|
// the token doesn't contain evil operators like {}.
|
|
|
|
if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags)) {
|
|
|
|
completion_insert(c.completion, token_end, c.flags);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
bool use_prefix = false;
|
|
|
|
wcstring common_prefix;
|
|
|
|
if (all_matches_exact_or_prefix) {
|
|
|
|
// Try to find a common prefix to insert among the surviving completions.
|
|
|
|
complete_flags_t flags = 0;
|
|
|
|
bool prefix_is_partial_completion = false;
|
|
|
|
bool first = true;
|
|
|
|
for (const completion_t &el : surviving_completions) {
|
|
|
|
if (first) {
|
|
|
|
// First entry, use the whole string.
|
|
|
|
common_prefix = el.completion;
|
|
|
|
flags = el.flags;
|
|
|
|
first = false;
|
|
|
|
} else {
|
|
|
|
// Determine the shared prefix length.
|
|
|
|
size_t idx, max = std::min(common_prefix.size(), el.completion.size());
|
|
|
|
|
|
|
|
for (idx = 0; idx < max; idx++) {
|
|
|
|
wchar_t ac = common_prefix.at(idx), bc = el.completion.at(idx);
|
|
|
|
bool matches = (ac == bc);
|
|
|
|
// If we are replacing the token, allow case to vary.
|
|
|
|
if (will_replace_token && !matches) {
|
|
|
|
// Hackish way to compare two strings in a case insensitive way,
|
|
|
|
// hopefully better than towlower().
|
|
|
|
matches = (wcsncasecmp(&ac, &bc, 1) == 0);
|
|
|
|
}
|
|
|
|
if (!matches) break;
|
|
|
|
}
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
// idx is now the length of the new common prefix.
|
|
|
|
common_prefix.resize(idx);
|
|
|
|
prefix_is_partial_completion = true;
|
|
|
|
|
|
|
|
// Early out if we decide there's no common prefix.
|
|
|
|
if (idx == 0) break;
|
|
|
|
}
|
2020-04-21 19:41:21 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
// Determine if we use the prefix. We use it if it's non-empty and it will actually make
|
|
|
|
// the command line longer. It may make the command line longer by virtue of not using
|
|
|
|
// REPLACE_TOKEN (so it always appends to the command line), or by virtue of replacing
|
|
|
|
// the token but being longer than it.
|
|
|
|
use_prefix = common_prefix.size() > (will_replace_token ? tok.size() : 0);
|
|
|
|
assert(!use_prefix || !common_prefix.empty());
|
|
|
|
|
|
|
|
if (use_prefix) {
|
|
|
|
// We got something. If more than one completion contributed, then it means we have
|
|
|
|
// a prefix; don't insert a space after it.
|
|
|
|
if (prefix_is_partial_completion) flags |= COMPLETE_NO_SPACE;
|
|
|
|
completion_insert(common_prefix, token_end, flags);
|
|
|
|
cycle_command_line = command_line.text();
|
|
|
|
cycle_cursor_pos = command_line.position();
|
|
|
|
}
|
|
|
|
}
|
2020-11-29 04:12:27 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
if (use_prefix) {
|
|
|
|
for (completion_t &c : surviving_completions) {
|
|
|
|
c.flags &= ~COMPLETE_REPLACES_TOKEN;
|
|
|
|
c.completion.erase(0, common_prefix.size());
|
|
|
|
}
|
|
|
|
}
|
2020-11-29 04:12:27 +00:00
|
|
|
|
2020-12-02 18:11:36 +00:00
|
|
|
// Print the completion list.
|
|
|
|
wcstring prefix;
|
|
|
|
if (will_replace_token || !all_matches_exact_or_prefix) {
|
|
|
|
if (use_prefix) prefix = std::move(common_prefix);
|
|
|
|
} else if (tok.size() + common_prefix.size() <= PREFIX_MAX_LEN) {
|
|
|
|
prefix = tok + common_prefix;
|
|
|
|
} else {
|
|
|
|
// Append just the end of the string.
|
|
|
|
prefix = wcstring{get_ellipsis_char()};
|
|
|
|
prefix.append(tok + common_prefix, tok.size() + common_prefix.size() - PREFIX_MAX_LEN,
|
|
|
|
PREFIX_MAX_LEN);
|
2016-10-30 21:16:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the pager data.
|
2020-12-02 18:11:36 +00:00
|
|
|
pager.set_prefix(prefix);
|
|
|
|
pager.set_completions(surviving_completions);
|
2016-10-30 21:16:32 +00:00
|
|
|
// Invalidate our rendering.
|
2020-12-02 18:11:36 +00:00
|
|
|
current_page_rendering = page_rendering_t();
|
2016-10-30 21:16:32 +00:00
|
|
|
// Modify the command line to reflect the new pager.
|
2019-03-03 23:00:29 +00:00
|
|
|
pager_selection_changed();
|
2016-10-30 21:16:32 +00:00
|
|
|
return false;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Return true if we believe ourselves to be orphaned. loop_count is how many times we've tried to
|
|
|
|
/// stop ourselves via SIGGTIN.
|
|
|
|
static bool check_for_orphaned_process(unsigned long loop_count, pid_t shell_pgid) {
|
2012-12-05 21:33:07 +00:00
|
|
|
bool we_think_we_are_orphaned = false;
|
2016-05-03 17:36:22 +00:00
|
|
|
// Try kill-0'ing the process whose pid corresponds to our process group ID. It's possible this
|
|
|
|
// will fail because we don't have permission to signal it. But more likely it will fail because
|
|
|
|
// it no longer exists, and we are orphaned.
|
2016-10-22 18:21:13 +00:00
|
|
|
if (loop_count % 64 == 0 && kill(shell_pgid, 0) < 0 && errno == ESRCH) {
|
|
|
|
we_think_we_are_orphaned = true;
|
2012-12-05 21:33:07 +00:00
|
|
|
}
|
|
|
|
|
2017-05-18 06:07:47 +00:00
|
|
|
// Try reading from the tty; if we get EIO we are orphaned. This is sort of bad because it
|
|
|
|
// may block.
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!we_think_we_are_orphaned && loop_count % 128 == 0) {
|
2017-05-10 04:39:25 +00:00
|
|
|
#ifdef HAVE_CTERMID_R
|
|
|
|
char buf[L_ctermid];
|
|
|
|
char *tty = ctermid_r(buf);
|
|
|
|
#else
|
2019-11-19 02:34:50 +00:00
|
|
|
char *tty = ctermid(nullptr);
|
2017-05-10 04:39:25 +00:00
|
|
|
#endif
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!tty) {
|
2012-12-05 21:33:07 +00:00
|
|
|
wperror(L"ctermid");
|
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Open the tty. Presumably this is stdin, but maybe not?
|
2020-01-29 22:01:55 +00:00
|
|
|
autoclose_fd_t tty_fd{open(tty, O_RDONLY | O_NONBLOCK)};
|
|
|
|
if (!tty_fd.valid()) {
|
2012-12-05 21:33:07 +00:00
|
|
|
wperror(L"open");
|
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
char tmp;
|
2020-01-29 22:01:55 +00:00
|
|
|
if (read(tty_fd.fd(), &tmp, 1) < 0 && errno == EIO) {
|
2012-12-05 21:33:07 +00:00
|
|
|
we_think_we_are_orphaned = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Just give up if we've done it a lot times.
|
|
|
|
if (loop_count > 4096) {
|
2012-12-05 21:33:07 +00:00
|
|
|
we_think_we_are_orphaned = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return we_think_we_are_orphaned;
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-01-31 19:11:03 +00:00
|
|
|
// Ensure that fish owns the terminal, possibly waiting. If we cannot acquire the terminal, then
|
|
|
|
// report an error and exit.
|
|
|
|
static void acquire_tty_or_exit(pid_t shell_pgid) {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
2016-05-03 17:36:22 +00:00
|
|
|
|
|
|
|
// Check if we are in control of the terminal, so that we don't do semi-expensive things like
|
|
|
|
// reset signal handlers unless we really have to, which we often don't.
|
2020-01-31 19:11:03 +00:00
|
|
|
// Common case.
|
2020-10-18 21:37:31 +00:00
|
|
|
pid_t owner = tcgetpgrp(STDIN_FILENO);
|
|
|
|
if (owner == shell_pgid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// In some strange cases the tty may be come preassigned to fish's pid, but not its pgroup.
|
|
|
|
// In that case we simply attempt to claim our own pgroup.
|
|
|
|
// See #7388.
|
|
|
|
if (owner == getpid()) {
|
|
|
|
(void)setpgid(owner, owner);
|
2020-01-31 19:11:03 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bummer, we are not in control of the terminal. Stop until parent has given us control of
|
|
|
|
// it.
|
|
|
|
//
|
|
|
|
// In theory, reseting signal handlers could cause us to miss signal deliveries. In
|
|
|
|
// practice, this code should only be run during startup, when we're not waiting for any
|
|
|
|
// signals.
|
|
|
|
signal_reset_handlers();
|
|
|
|
cleanup_t restore_sigs([] { signal_set_handlers(true); });
|
|
|
|
|
|
|
|
// Ok, signal handlers are taken out of the picture. Stop ourself in a loop until we are in
|
|
|
|
// control of the terminal. However, the call to signal(SIGTTIN) may silently not do
|
|
|
|
// anything if we are orphaned.
|
|
|
|
//
|
|
|
|
// As far as I can tell there's no really good way to detect that we are orphaned. One way
|
|
|
|
// is to just detect if the group leader exited, via kill(shell_pgid, 0). Another
|
|
|
|
// possibility is that read() from the tty fails with EIO - this is more reliable but it's
|
|
|
|
// harder, because it may succeed or block. So we loop for a while, trying those strategies.
|
|
|
|
// Eventually we just give up and assume we're orphaend.
|
|
|
|
for (unsigned loop_count = 0;; loop_count++) {
|
2020-10-18 21:37:31 +00:00
|
|
|
owner = tcgetpgrp(STDIN_FILENO);
|
2020-01-31 19:11:03 +00:00
|
|
|
// 0 is a valid return code from `tcgetpgrp()` under at least FreeBSD and testing
|
|
|
|
// indicates that a subsequent call to `tcsetpgrp()` will succeed. 0 is the
|
|
|
|
// pid of the top-level kernel process, so I'm not sure if this means ownership
|
|
|
|
// of the terminal has gone back to the kernel (i.e. it's not owned) or if it is
|
|
|
|
// just an "invalid" pid for all intents and purposes.
|
|
|
|
if (owner == 0) {
|
|
|
|
tcsetpgrp(STDIN_FILENO, shell_pgid);
|
|
|
|
// Since we expect the above to work, call `tcgetpgrp()` immediately to
|
|
|
|
// avoid a second pass through this loop.
|
|
|
|
owner = tcgetpgrp(STDIN_FILENO);
|
|
|
|
}
|
|
|
|
if (owner == -1 && errno == ENOTTY) {
|
2020-12-06 21:40:45 +00:00
|
|
|
if (!is_interactive_session()) {
|
2020-01-31 19:11:03 +00:00
|
|
|
// It's OK if we're not able to take control of the terminal. We handle
|
|
|
|
// the fallout from this in a few other places.
|
|
|
|
break;
|
2018-09-29 04:13:13 +00:00
|
|
|
}
|
2020-01-31 19:11:03 +00:00
|
|
|
// No TTY, cannot be interactive?
|
|
|
|
redirect_tty_output();
|
|
|
|
FLOGF(warning, _(L"No TTY for interactive shell (tcgetpgrp failed)"));
|
|
|
|
wperror(L"setpgid");
|
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
|
|
|
if (owner == shell_pgid) {
|
|
|
|
break; // success
|
|
|
|
} else {
|
|
|
|
if (check_for_orphaned_process(loop_count, shell_pgid)) {
|
|
|
|
// We're orphaned, so we just die. Another sad statistic.
|
|
|
|
const wchar_t *fmt =
|
2020-11-22 13:39:48 +00:00
|
|
|
_(L"I appear to be an orphaned process, so I am quitting politely. My pid is "
|
|
|
|
L"%d.");
|
2020-04-08 23:56:59 +00:00
|
|
|
FLOGF(warning, fmt, static_cast<int>(getpid()));
|
2012-12-05 21:33:07 +00:00
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
|
|
|
|
2020-01-31 19:11:03 +00:00
|
|
|
// Try stopping us.
|
|
|
|
int ret = killpg(shell_pgid, SIGTTIN);
|
|
|
|
if (ret < 0) {
|
|
|
|
wperror(L"killpg(shell_pgid, SIGTTIN)");
|
|
|
|
exit_without_destructors(1);
|
2012-12-05 21:33:07 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
2020-01-31 19:11:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Initialize data for interactive use.
|
|
|
|
static void reader_interactive_init(parser_t &parser) {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
|
|
|
|
pid_t shell_pgid = getpgrp();
|
2020-12-06 21:40:45 +00:00
|
|
|
pid_t shell_pid = getpid();
|
2020-01-31 19:11:03 +00:00
|
|
|
|
|
|
|
// Set up key bindings.
|
|
|
|
init_input();
|
|
|
|
|
|
|
|
// Ensure interactive signal handling is enabled.
|
|
|
|
signal_set_handlers_once(true);
|
|
|
|
|
|
|
|
// Wait until we own the terminal.
|
|
|
|
acquire_tty_or_exit(shell_pgid);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-12-06 21:40:45 +00:00
|
|
|
// If fish has no valid pgroup (possible with firejail, see #5295) or is interactive,
|
|
|
|
// ensure it owns the terminal. Also see #5909, #7060.
|
|
|
|
if (shell_pgid == 0 || (is_interactive_session() && shell_pgid != shell_pid)) {
|
|
|
|
shell_pgid = shell_pid;
|
2018-10-31 17:46:41 +00:00
|
|
|
if (setpgid(shell_pgid, shell_pgid) < 0) {
|
2020-01-16 23:14:21 +00:00
|
|
|
// If we're session leader setpgid returns EPERM. The other cases where we'd get EPERM
|
|
|
|
// don't apply as we passed our own pid.
|
2020-01-13 17:56:23 +00:00
|
|
|
//
|
|
|
|
// This should be harmless, so we ignore it.
|
|
|
|
if (errno != EPERM) {
|
|
|
|
FLOG(error, _(L"Failed to assign shell to its own process group"));
|
|
|
|
wperror(L"setpgid");
|
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
2018-10-31 17:46:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Take control of the terminal
|
|
|
|
if (tcsetpgrp(STDIN_FILENO, shell_pgid) == -1) {
|
|
|
|
if (errno == ENOTTY) {
|
|
|
|
redirect_tty_output();
|
|
|
|
}
|
2019-05-27 22:56:53 +00:00
|
|
|
FLOG(error, _(L"Failed to take control of the terminal"));
|
2018-10-31 17:46:41 +00:00
|
|
|
wperror(L"tcsetpgrp");
|
|
|
|
exit_without_destructors(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Configure terminal attributes
|
2020-09-30 16:29:00 +00:00
|
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes) == -1) {
|
2018-10-31 17:46:41 +00:00
|
|
|
if (errno == EIO) {
|
|
|
|
redirect_tty_output();
|
|
|
|
}
|
2020-01-19 12:38:47 +00:00
|
|
|
FLOGF(warning, _(L"Failed to set startup terminal mode!"));
|
2018-10-31 17:46:41 +00:00
|
|
|
wperror(L"tcsetattr");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-08 02:35:47 +00:00
|
|
|
termsize_container_t::shared().invalidate_tty();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
// For compatibility with fish 2.0's $_, now replaced with `status current-command`
|
2019-05-27 21:52:48 +00:00
|
|
|
parser.vars().set_one(L"_", ENV_GLOBAL, L"fish");
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Destroy data for interactive use.
|
|
|
|
static void reader_interactive_destroy() {
|
2018-10-06 20:32:08 +00:00
|
|
|
outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset());
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Set the specified string as the current buffer.
|
2020-02-04 11:47:44 +00:00
|
|
|
void reader_data_t::set_command_line_and_position(editable_line_t *el, wcstring &&new_str,
|
2019-03-03 23:00:29 +00:00
|
|
|
size_t pos) {
|
2020-02-09 17:39:14 +00:00
|
|
|
push_edit(el, edit_t(0, el->size(), std::move(new_str)));
|
2020-02-04 11:47:44 +00:00
|
|
|
el->set_position(pos);
|
2019-03-03 23:00:29 +00:00
|
|
|
update_buff_pos(el, pos);
|
2012-06-12 16:41:30 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
/// Undo the transient edit und update commandline accordingly.
|
|
|
|
void reader_data_t::clear_transient_edit() {
|
2020-02-29 20:19:28 +00:00
|
|
|
if (!command_line_has_transient_edit) {
|
|
|
|
return;
|
|
|
|
}
|
2020-02-04 11:47:44 +00:00
|
|
|
command_line.undo();
|
|
|
|
update_buff_pos(&command_line);
|
|
|
|
command_line_has_transient_edit = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::replace_current_token(wcstring &&new_token) {
|
2012-11-19 00:30:30 +00:00
|
|
|
const wchar_t *begin, *end;
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Find current token.
|
2019-03-03 23:47:00 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!begin || !end) return;
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t offset = begin - buff;
|
2020-03-20 13:05:26 +00:00
|
|
|
size_t length = end - begin;
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(el, offset, length, std::move(new_token));
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
/// Apply the history search to the command line.
|
2019-03-03 23:00:29 +00:00
|
|
|
void reader_data_t::update_command_line_from_history_search() {
|
|
|
|
wcstring new_text = history_search.is_at_end() ? history_search.search_string()
|
|
|
|
: history_search.current_result();
|
2020-02-04 11:47:44 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (command_line_has_transient_edit) {
|
|
|
|
el->undo();
|
|
|
|
}
|
2019-03-03 23:00:29 +00:00
|
|
|
if (history_search.by_token()) {
|
2020-02-04 11:47:44 +00:00
|
|
|
replace_current_token(std::move(new_text));
|
2020-02-08 07:55:02 +00:00
|
|
|
} else {
|
2020-06-30 20:20:14 +00:00
|
|
|
assert(history_search.by_line() || history_search.by_prefix());
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(&command_line, 0, command_line.size(), std::move(new_text));
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2020-02-04 11:47:44 +00:00
|
|
|
command_line_has_transient_edit = true;
|
|
|
|
assert(el == &command_line);
|
|
|
|
update_buff_pos(el);
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-06-06 01:46:04 +00:00
|
|
|
enum move_word_dir_t { MOVE_DIR_LEFT, MOVE_DIR_RIGHT };
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Move buffer position one word or erase one word. This function updates both the internal buffer
|
|
|
|
/// and the screen. It is used by M-left, M-right and ^W to do block movement or block erase.
|
|
|
|
///
|
2016-06-06 01:46:04 +00:00
|
|
|
/// \param move_right true if moving right
|
2016-05-03 17:36:22 +00:00
|
|
|
/// \param erase Whether to erase the characters along the way or only move past them.
|
2016-06-06 01:46:04 +00:00
|
|
|
/// \param newv if the new kill item should be appended to the previous kill item or not.
|
2019-03-03 21:59:35 +00:00
|
|
|
void reader_data_t::move_word(editable_line_t *el, bool move_right, bool erase,
|
|
|
|
enum move_word_style_t style, bool newv) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Return if we are already at the edge.
|
2014-01-26 22:55:41 +00:00
|
|
|
const size_t boundary = move_right ? el->size() : 0;
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() == boundary) return;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// When moving left, a value of 1 means the character at index 0.
|
2012-12-21 01:37:09 +00:00
|
|
|
move_word_state_machine_t state(style);
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *const command_line = el->text().c_str();
|
|
|
|
const size_t start_buff_pos = el->position();
|
2012-11-19 10:41:57 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t buff_pos = el->position();
|
2016-05-03 17:36:22 +00:00
|
|
|
while (buff_pos != boundary) {
|
2012-11-19 10:41:57 +00:00
|
|
|
size_t idx = (move_right ? buff_pos : buff_pos - 1);
|
|
|
|
wchar_t c = command_line[idx];
|
2016-05-03 17:36:22 +00:00
|
|
|
if (!state.consume_char(c)) break;
|
2012-11-19 10:41:57 +00:00
|
|
|
buff_pos = (move_right ? buff_pos + 1 : buff_pos - 1);
|
2012-10-04 21:35:03 +00:00
|
|
|
}
|
2006-10-12 16:13:17 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Always consume at least one character.
|
|
|
|
if (buff_pos == start_buff_pos) buff_pos = (move_right ? buff_pos + 1 : buff_pos - 1);
|
2012-11-19 10:41:57 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// If we are moving left, buff_pos-1 is the index of the first character we do not delete
|
|
|
|
// (possibly -1). If we are moving right, then buff_pos is that index - possibly el->size().
|
|
|
|
if (erase) {
|
|
|
|
// Don't autosuggest after a kill.
|
2019-03-03 21:59:35 +00:00
|
|
|
if (el == &this->command_line) {
|
|
|
|
suppress_autosuggestion = true;
|
2014-01-26 22:55:41 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (move_right) {
|
2019-03-03 21:59:35 +00:00
|
|
|
kill(el, start_buff_pos, buff_pos - start_buff_pos, KILL_APPEND, newv);
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-03-03 21:59:35 +00:00
|
|
|
kill(el, buff_pos, start_buff_pos - buff_pos, KILL_PREPEND, newv);
|
2012-11-19 10:41:57 +00:00
|
|
|
}
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2014-03-29 21:19:45 +00:00
|
|
|
update_buff_pos(el, buff_pos);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2012-11-19 10:41:57 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Sets the command line contents, without clearing the pager.
|
2020-02-04 11:47:44 +00:00
|
|
|
void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Callers like to pass us pointers into ourselves, so be careful! I don't know if we can use
|
|
|
|
// operator= with a pointer to our interior, so use an intermediate.
|
2012-11-19 00:30:30 +00:00
|
|
|
size_t command_line_len = b.size();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (transient) {
|
|
|
|
if (command_line_has_transient_edit) {
|
|
|
|
command_line.undo();
|
|
|
|
}
|
|
|
|
command_line_has_transient_edit = true;
|
|
|
|
}
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(&command_line, 0, command_line.size(), wcstring(b));
|
2019-03-03 23:00:29 +00:00
|
|
|
command_line_changed(&command_line);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Don't set a position past the command line length.
|
2016-10-21 01:53:31 +00:00
|
|
|
if (pos > command_line_len) pos = command_line_len; //!OCLINT(parameter reassignment)
|
2019-03-03 23:00:29 +00:00
|
|
|
update_buff_pos(&command_line, pos);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Clear history search and pager contents.
|
2019-03-03 23:00:29 +00:00
|
|
|
history_search.reset();
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2020-09-08 20:04:44 +00:00
|
|
|
static void set_env_cmd_duration(struct timeval *after, struct timeval *before, env_stack_t &vars) {
|
2012-11-19 00:30:30 +00:00
|
|
|
time_t secs = after->tv_sec - before->tv_sec;
|
|
|
|
suseconds_t usecs = after->tv_usec - before->tv_usec;
|
2009-11-24 15:28:42 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (after->tv_usec < before->tv_usec) {
|
2012-11-19 00:30:30 +00:00
|
|
|
usecs += 1000000;
|
|
|
|
secs -= 1;
|
|
|
|
}
|
2009-11-24 15:28:42 +00:00
|
|
|
|
2019-02-19 07:12:03 +00:00
|
|
|
vars.set_one(ENV_CMD_DURATION, ENV_UNEXPORT, std::to_wstring((secs * 1000) + (usecs / 1000)));
|
2009-11-24 15:28:42 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-09-08 20:04:44 +00:00
|
|
|
/// Run the specified command with the correct terminal modes, and while taking care to perform job
|
|
|
|
/// notification, set the title, etc.
|
|
|
|
static eval_res_t reader_run_command(parser_t &parser, const wcstring &cmd) {
|
2012-11-19 00:30:30 +00:00
|
|
|
struct timeval time_before, time_after;
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-02-23 23:16:12 +00:00
|
|
|
wcstring ft = tok_command(cmd);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
// For compatibility with fish 2.0's $_, now replaced with `status current-command`
|
2018-09-14 07:36:26 +00:00
|
|
|
if (!ft.empty()) parser.vars().set_one(L"_", ENV_GLOBAL, ft);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2018-10-06 20:32:08 +00:00
|
|
|
outputter_t &outp = outputter_t::stdoutput();
|
2019-05-05 03:20:52 +00:00
|
|
|
reader_write_title(cmd, parser);
|
Do not reset terminal color when donating term for running key bindings
fish maintains two tty modes: one for itself and one for external
commands. The external command mode is also used when executing
fish-script key bindings, which was added in 5f16a299a728 (note that
commit had the wrong issue, the correct issue is #2114).
Prior to this fix, when switching to external modes, we would also reset
the tty's foreground color. This bumped tty's timestamp, causing us to
believe that the tty had been modified, and then repainting the prompt. If
the prompt were multi-line, we would repaint the whole prompt starting
from its second line, leaving a trailing line above it.
It would be reasonable to save the tty timestamp after resetting the
color, but given that using external modes for keybindings is new, it's
better to instead not reset the color in this case. So migrate the color
resetting to only when we run external commands.
Fixes #7722
2021-02-17 19:04:22 +00:00
|
|
|
outp.set_color(rgb_color_t::normal(), rgb_color_t::normal());
|
|
|
|
term_donate();
|
2006-01-30 19:53:10 +00:00
|
|
|
|
2019-11-19 02:34:50 +00:00
|
|
|
gettimeofday(&time_before, nullptr);
|
2009-11-24 15:28:42 +00:00
|
|
|
|
2020-08-02 20:37:19 +00:00
|
|
|
auto eval_res = parser.eval(cmd, io_chain_t{});
|
2019-04-30 03:58:58 +00:00
|
|
|
job_reap(parser, true);
|
2006-01-30 19:53:10 +00:00
|
|
|
|
2019-11-19 02:34:50 +00:00
|
|
|
gettimeofday(&time_after, nullptr);
|
2018-08-11 21:30:10 +00:00
|
|
|
|
2018-05-27 08:38:29 +00:00
|
|
|
// update the execution duration iff a command is requested for execution
|
|
|
|
// issue - #4926
|
2018-09-14 07:36:26 +00:00
|
|
|
if (!ft.empty()) set_env_cmd_duration(&time_after, &time_before, parser.vars());
|
2009-11-24 15:28:42 +00:00
|
|
|
|
2012-11-19 00:30:30 +00:00
|
|
|
term_steal();
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
// For compatibility with fish 2.0's $_, now replaced with `status current-command`
|
2018-09-14 07:36:26 +00:00
|
|
|
parser.vars().set_one(L"_", ENV_GLOBAL, program_name);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2019-05-12 21:59:30 +00:00
|
|
|
if (have_proc_stat()) {
|
2019-05-05 05:12:31 +00:00
|
|
|
proc_update_jiffies(parser);
|
2019-04-29 13:29:21 +00:00
|
|
|
}
|
2020-08-02 20:37:19 +00:00
|
|
|
|
|
|
|
return eval_res;
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2020-11-15 10:26:15 +00:00
|
|
|
static parser_test_error_bits_t reader_shell_test(const parser_t &parser, const wcstring &b) {
|
2013-12-13 02:18:07 +00:00
|
|
|
wcstring bstr = b;
|
2014-01-15 09:40:40 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Append a newline, to act as a statement terminator.
|
2014-01-09 02:20:55 +00:00
|
|
|
bstr.push_back(L'\n');
|
2014-01-15 09:40:40 +00:00
|
|
|
|
2013-12-16 00:05:37 +00:00
|
|
|
parse_error_list_t errors;
|
2016-05-03 17:36:22 +00:00
|
|
|
parser_test_error_bits_t res =
|
|
|
|
parse_util_detect_errors(bstr, &errors, true /* do accept incomplete */);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
if (res & PARSER_TEST_ERROR) {
|
2014-01-09 02:20:55 +00:00
|
|
|
wcstring error_desc;
|
2019-05-27 01:51:26 +00:00
|
|
|
parser.get_backtrace(bstr, errors, error_desc);
|
2014-01-15 09:40:40 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Ensure we end with a newline. Also add an initial newline, because it's likely the user
|
|
|
|
// just hit enter and so there's junk on the current line.
|
|
|
|
if (!string_suffixes_string(L"\n", error_desc)) {
|
2014-01-09 02:20:55 +00:00
|
|
|
error_desc.push_back(L'\n');
|
2013-12-17 00:52:23 +00:00
|
|
|
}
|
2019-03-12 21:06:01 +00:00
|
|
|
std::fwprintf(stderr, L"\n%ls", error_desc.c_str());
|
2020-08-27 19:18:26 +00:00
|
|
|
reader_schedule_prompt_repaint();
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
return res;
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
wcstring reader_data_t::history_search_text_if_active() const {
|
|
|
|
if (!history_search.active() || history_search.is_at_end()) {
|
|
|
|
return wcstring{};
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
2020-08-23 22:12:47 +00:00
|
|
|
return history_search.search_string();
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::highlight_complete(highlight_result_t result) {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
2020-08-23 22:12:47 +00:00
|
|
|
in_flight_highlight_request.clear();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (result.text == command_line.text()) {
|
2019-03-03 23:47:00 +00:00
|
|
|
assert(result.colors.size() == command_line.size());
|
2020-08-23 22:12:47 +00:00
|
|
|
if (this->is_repaint_needed(&result.colors)) {
|
|
|
|
this->layout_and_repaint(L"highlight", std::move(result.colors));
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-11 22:42:22 +00:00
|
|
|
// Given text and whether IO is allowed, return a function that performs highlighting. The function
|
|
|
|
// may be invoked on a background thread.
|
2020-08-03 21:10:37 +00:00
|
|
|
static std::function<highlight_result_t(void)> get_highlight_performer(parser_t &parser,
|
|
|
|
const wcstring &text,
|
|
|
|
bool io_ok) {
|
2019-05-27 01:51:26 +00:00
|
|
|
auto vars = parser.vars().snapshot();
|
2020-08-22 19:30:33 +00:00
|
|
|
uint32_t generation_count = read_generation_count();
|
2019-03-03 23:47:00 +00:00
|
|
|
return [=]() -> highlight_result_t {
|
|
|
|
if (text.empty()) return {};
|
2020-01-16 01:14:47 +00:00
|
|
|
operation_context_t ctx = get_bg_context(vars, generation_count);
|
2019-03-04 01:34:00 +00:00
|
|
|
std::vector<highlight_spec_t> colors(text.size(), highlight_spec_t{});
|
2020-08-11 22:42:22 +00:00
|
|
|
highlight_shell(text, colors, ctx, io_ok);
|
|
|
|
return highlight_result_t{std::move(colors), text};
|
2019-03-03 23:47:00 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
/// Highlight the command line in a super, plentiful way.
|
2020-11-02 02:25:09 +00:00
|
|
|
void reader_data_t::super_highlight_me_plenty() {
|
2020-08-03 21:53:09 +00:00
|
|
|
if (!conf.highlight_ok) return;
|
2020-08-03 21:10:37 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Do nothing if this text is already in flight.
|
2020-11-02 02:25:09 +00:00
|
|
|
const editable_line_t *el = &command_line;
|
2020-08-23 22:12:47 +00:00
|
|
|
if (el->text() == in_flight_highlight_request) return;
|
|
|
|
in_flight_highlight_request = el->text();
|
|
|
|
|
|
|
|
FLOG(reader_render, L"Highlighting");
|
2020-11-02 02:25:09 +00:00
|
|
|
auto highlight_performer = get_highlight_performer(parser(), el->text(), true /* io_ok */);
|
|
|
|
auto shared_this = this->shared_from_this();
|
|
|
|
debounce_highlighting().perform(highlight_performer, [shared_this](highlight_result_t result) {
|
|
|
|
shared_this->highlight_complete(std::move(result));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void reader_data_t::finish_highlighting_before_exec() {
|
|
|
|
if (!conf.highlight_ok) return;
|
|
|
|
if (in_flight_highlight_request.empty()) return;
|
|
|
|
|
|
|
|
// We have an in-flight highlight request scheduled.
|
|
|
|
// Wait for its completion to run, but not forever.
|
|
|
|
namespace sc = std::chrono;
|
|
|
|
auto now = sc::steady_clock::now();
|
|
|
|
auto deadline = now + sc::milliseconds(kHighlightTimeoutForExecutionMs);
|
|
|
|
while (now < deadline) {
|
|
|
|
long timeout_usec = sc::duration_cast<sc::microseconds>(deadline - now).count();
|
2021-02-07 02:43:43 +00:00
|
|
|
iothread_service_main_with_timeout(timeout_usec);
|
2020-11-02 02:25:09 +00:00
|
|
|
|
2021-02-07 02:43:43 +00:00
|
|
|
// Note iothread_service_main_with_timeout will reentrantly modify us,
|
2020-11-02 02:25:09 +00:00
|
|
|
// by invoking a completion.
|
|
|
|
if (in_flight_highlight_request.empty()) break;
|
|
|
|
now = sc::steady_clock::now();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!in_flight_highlight_request.empty()) {
|
|
|
|
// We did not complete before the deadline.
|
|
|
|
// Give up and highlight without I/O.
|
|
|
|
const editable_line_t *el = &command_line;
|
|
|
|
auto highlight_no_io = get_highlight_performer(parser(), el->text(), false /* io not ok */);
|
|
|
|
this->highlight_complete(highlight_no_io());
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The stack of current interactive reading contexts.
|
|
|
|
static std::vector<std::shared_ptr<reader_data_t>> reader_data_stack;
|
|
|
|
|
|
|
|
/// Access the top level reader data.
|
|
|
|
static reader_data_t *current_data_or_null() {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
return reader_data_stack.empty() ? nullptr : reader_data_stack.back().get();
|
|
|
|
}
|
|
|
|
|
|
|
|
static reader_data_t *current_data() {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
assert(!reader_data_stack.empty() && "no current reader");
|
|
|
|
return reader_data_stack.back().get();
|
2016-10-09 21:38:26 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2018-09-09 05:13:54 +00:00
|
|
|
void reader_change_history(const wcstring &name) {
|
2018-01-01 12:00:00 +00:00
|
|
|
// We don't need to _change_ if we're not initialized yet.
|
2018-08-11 20:16:07 +00:00
|
|
|
reader_data_t *data = current_data_or_null();
|
2018-01-01 12:00:00 +00:00
|
|
|
if (data && data->history) {
|
|
|
|
data->history->save();
|
2021-01-10 00:22:32 +00:00
|
|
|
data->history = history_t::with_name(name);
|
2018-01-01 12:00:00 +00:00
|
|
|
}
|
2017-07-28 04:32:49 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 22:41:12 +00:00
|
|
|
/// Add a new reader to the reader stack.
|
|
|
|
/// \return a shared pointer to it.
|
|
|
|
static std::shared_ptr<reader_data_t> reader_push_ret(parser_t &parser,
|
|
|
|
const wcstring &history_name,
|
|
|
|
reader_config_t &&conf) {
|
2021-01-10 00:22:32 +00:00
|
|
|
std::shared_ptr<history_t> hist = history_t::with_name(history_name);
|
2020-08-03 22:41:12 +00:00
|
|
|
auto data = std::make_shared<reader_data_t>(parser.shared(), hist, std::move(conf));
|
|
|
|
reader_data_stack.push_back(data);
|
2014-01-27 08:56:13 +00:00
|
|
|
data->command_line_changed(&data->command_line);
|
2018-08-11 20:16:07 +00:00
|
|
|
if (reader_data_stack.size() == 1) {
|
2019-05-27 21:52:48 +00:00
|
|
|
reader_interactive_init(parser);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2020-08-03 22:41:12 +00:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Public variant which discards the return value.
|
|
|
|
void reader_push(parser_t &parser, const wcstring &history_name, reader_config_t &&conf) {
|
|
|
|
(void)reader_push_ret(parser, history_name, std::move(conf));
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
void reader_pop() {
|
2018-08-11 20:16:07 +00:00
|
|
|
assert(!reader_data_stack.empty() && "empty stack in reader_data_stack");
|
|
|
|
reader_data_stack.pop_back();
|
|
|
|
reader_data_t *new_reader = current_data_or_null();
|
|
|
|
if (new_reader == nullptr) {
|
2012-11-19 00:30:30 +00:00
|
|
|
reader_interactive_destroy();
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2020-06-08 02:35:47 +00:00
|
|
|
s_reset_abandoning_line(&new_reader->screen, termsize_last().width);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 22:41:12 +00:00
|
|
|
void reader_data_t::import_history_if_necessary() {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Import history from older location (config path) if our current history is empty.
|
2020-08-03 22:41:12 +00:00
|
|
|
if (history && history->is_empty()) {
|
|
|
|
history->populate_from_config_path();
|
2015-09-19 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2018-09-18 16:18:47 +00:00
|
|
|
// Import history from bash, etc. if our current history is still empty and is the default
|
|
|
|
// history.
|
2020-08-03 22:41:12 +00:00
|
|
|
if (history && history->is_empty() && history->is_default()) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Try opening a bash file. We make an effort to respect $HISTFILE; this isn't very complete
|
|
|
|
// (AFAIK it doesn't have to be exported), and to really get this right we ought to ask bash
|
|
|
|
// itself. But this is better than nothing.
|
2020-08-03 22:41:12 +00:00
|
|
|
const auto var = vars().get(L"HISTFILE");
|
2017-08-28 07:25:41 +00:00
|
|
|
wcstring path = (var ? var->as_string() : L"~/.bash_history");
|
2020-08-03 22:41:12 +00:00
|
|
|
expand_tilde(path, vars());
|
2020-01-28 18:43:37 +00:00
|
|
|
int fd = wopen_cloexec(path, O_RDONLY);
|
|
|
|
if (fd >= 0) {
|
|
|
|
FILE *f = fdopen(fd, "r");
|
2020-08-03 22:41:12 +00:00
|
|
|
history->populate_from_bash(f);
|
2012-07-10 05:54:08 +00:00
|
|
|
fclose(f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-12-27 03:18:46 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
/// Check if we have background jobs that we have not warned about.
|
|
|
|
/// If so, print a warning and return true. Otherwise return false.
|
|
|
|
static bool try_warn_on_background_jobs(reader_data_t *data) {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
// Have we already warned?
|
|
|
|
if (data->did_warn_for_bg_jobs) return false;
|
|
|
|
// Are we the top-level reader?
|
|
|
|
if (reader_data_stack.size() > 1) return false;
|
|
|
|
// Do we have background jobs?
|
|
|
|
auto bg_jobs = jobs_requiring_warning_on_exit(data->parser());
|
|
|
|
if (bg_jobs.empty()) return false;
|
|
|
|
// Print the warning!
|
|
|
|
print_exit_warning_for_jobs(bg_jobs);
|
|
|
|
data->did_warn_for_bg_jobs = true;
|
|
|
|
return true;
|
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
/// Check if we should exit the reader loop.
|
|
|
|
/// \return true if we should exit.
|
|
|
|
static bool check_exit_loop_maybe_warning(reader_data_t *data) {
|
|
|
|
// sighup always forces exit.
|
|
|
|
if (s_sighup_received) return true;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
// Check if an exit is requested.
|
|
|
|
if (data->exit_loop_requested) {
|
|
|
|
if (try_warn_on_background_jobs(data)) {
|
|
|
|
data->exit_loop_requested = false;
|
|
|
|
return false;
|
2017-01-24 23:15:38 +00:00
|
|
|
}
|
2020-08-15 21:41:11 +00:00
|
|
|
return true;
|
2016-12-20 04:35:57 +00:00
|
|
|
}
|
2020-08-15 21:41:11 +00:00
|
|
|
return false;
|
2007-09-26 09:01:59 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 22:41:12 +00:00
|
|
|
static bool selection_is_at_top(const reader_data_t *data) {
|
2014-01-27 10:17:31 +00:00
|
|
|
const pager_t *pager = &data->pager;
|
|
|
|
size_t row = pager->get_selected_row(data->current_page_rendering);
|
2016-05-03 17:36:22 +00:00
|
|
|
if (row != 0 && row != PAGER_SELECTION_NONE) return false;
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2014-01-27 10:17:31 +00:00
|
|
|
size_t col = pager->get_selected_column(data->current_page_rendering);
|
2019-11-26 00:51:28 +00:00
|
|
|
return !(col != 0 && col != PAGER_SELECTION_NONE);
|
2014-01-27 10:17:31 +00:00
|
|
|
}
|
|
|
|
|
2019-04-29 01:13:55 +00:00
|
|
|
static relaxed_atomic_t<uint64_t> run_count{0};
|
2018-08-21 03:50:27 +00:00
|
|
|
|
|
|
|
/// Returns the current interactive loop count
|
2019-04-29 01:13:55 +00:00
|
|
|
uint64_t reader_run_count() { return run_count; }
|
2018-08-21 03:50:27 +00:00
|
|
|
|
2020-08-02 20:37:19 +00:00
|
|
|
static relaxed_atomic_t<uint64_t> status_count{0};
|
|
|
|
|
|
|
|
/// Returns the current "generation" of interactive status.
|
|
|
|
/// This is not incremented if the command being run produces no status,
|
|
|
|
/// (e.g. background job, or variable assignment).
|
|
|
|
uint64_t reader_status_count() { return status_count; }
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Read interactively. Read input from stdin while providing editing facilities.
|
2019-05-27 01:51:26 +00:00
|
|
|
static int read_i(parser_t &parser) {
|
2020-08-15 21:41:11 +00:00
|
|
|
ASSERT_IS_MAIN_THREAD();
|
2020-08-03 21:53:09 +00:00
|
|
|
reader_config_t conf;
|
|
|
|
conf.complete_ok = true;
|
|
|
|
conf.highlight_ok = true;
|
|
|
|
conf.syntax_check_ok = true;
|
|
|
|
conf.autosuggest_ok = true;
|
|
|
|
conf.expand_abbrev_ok = true;
|
|
|
|
|
|
|
|
if (parser.libdata().is_breakpoint && function_exists(DEBUG_PROMPT_FUNCTION_NAME, parser)) {
|
|
|
|
conf.left_prompt_cmd = DEBUG_PROMPT_FUNCTION_NAME;
|
2020-09-02 15:48:24 +00:00
|
|
|
conf.right_prompt_cmd = wcstring{};
|
2020-08-03 21:53:09 +00:00
|
|
|
} else {
|
|
|
|
conf.left_prompt_cmd = LEFT_PROMPT_FUNCTION_NAME;
|
|
|
|
conf.right_prompt_cmd = RIGHT_PROMPT_FUNCTION_NAME;
|
|
|
|
}
|
|
|
|
|
2020-08-03 22:41:12 +00:00
|
|
|
std::shared_ptr<reader_data_t> data =
|
|
|
|
reader_push_ret(parser, history_session_id(parser.vars()), std::move(conf));
|
|
|
|
data->import_history_if_necessary();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
while (!check_exit_loop_maybe_warning(data.get())) {
|
2020-11-15 10:25:45 +00:00
|
|
|
++run_count;
|
2017-06-20 05:57:32 +00:00
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
maybe_t<wcstring> tmp = data->readline(0);
|
2020-08-15 21:41:11 +00:00
|
|
|
if (tmp && !tmp->empty()) {
|
2019-02-24 21:59:49 +00:00
|
|
|
const wcstring command = tmp.acquire();
|
2019-03-03 21:59:35 +00:00
|
|
|
data->update_buff_pos(&data->command_line, 0);
|
2020-02-04 11:47:44 +00:00
|
|
|
data->command_line.clear();
|
2014-01-27 08:56:13 +00:00
|
|
|
data->command_line_changed(&data->command_line);
|
2020-08-15 21:41:11 +00:00
|
|
|
wcstring_list_t argv{command};
|
2020-06-07 00:16:45 +00:00
|
|
|
event_fire_generic(parser, L"fish_preexec", &argv);
|
2020-08-02 20:37:19 +00:00
|
|
|
auto eval_res = reader_run_command(parser, command);
|
2020-07-12 18:35:27 +00:00
|
|
|
signal_clear_cancel();
|
2020-08-02 20:37:19 +00:00
|
|
|
if (!eval_res.no_status) {
|
2020-11-15 10:25:45 +00:00
|
|
|
++status_count;
|
2020-08-02 20:37:19 +00:00
|
|
|
}
|
2020-08-15 21:41:11 +00:00
|
|
|
|
|
|
|
// If the command requested an exit, then process it now and clear it.
|
|
|
|
data->exit_loop_requested |= parser.libdata().exit_current_script;
|
|
|
|
parser.libdata().exit_current_script = false;
|
|
|
|
|
2020-06-07 00:16:45 +00:00
|
|
|
event_fire_generic(parser, L"fish_postexec", &argv);
|
2016-05-03 17:36:22 +00:00
|
|
|
// Allow any pending history items to be returned in the history array.
|
|
|
|
if (data->history) {
|
2015-04-20 09:04:17 +00:00
|
|
|
data->history->resolve_pending();
|
|
|
|
}
|
2020-08-15 21:41:11 +00:00
|
|
|
|
|
|
|
bool already_warned = data->did_warn_for_bg_jobs;
|
|
|
|
if (check_exit_loop_maybe_warning(data.get())) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (already_warned) {
|
|
|
|
// We had previously warned the user and they ran another command.
|
|
|
|
// Reset the warning.
|
|
|
|
data->did_warn_for_bg_jobs = false;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
reader_pop();
|
2020-08-15 21:41:11 +00:00
|
|
|
|
|
|
|
// If we got SIGHUP, ensure the tty is redirected.
|
|
|
|
if (s_sighup_received) {
|
|
|
|
// If we are the top-level reader, then we translate SIGHUP into exit_forced.
|
|
|
|
redirect_tty_after_sighup();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are the last reader, then kill remaining jobs before exiting.
|
|
|
|
if (reader_data_stack.size() == 0) {
|
2020-08-29 22:14:33 +00:00
|
|
|
// Send the exit event and then commit to not executing any more fish script.
|
|
|
|
s_exit_state = exit_state_t::running_handlers;
|
2020-08-15 21:41:11 +00:00
|
|
|
event_fire_generic(parser, L"fish_exit");
|
2020-08-29 22:14:33 +00:00
|
|
|
s_exit_state = exit_state_t::finished_handlers;
|
2020-08-15 21:41:11 +00:00
|
|
|
hup_jobs(parser.jobs());
|
|
|
|
}
|
|
|
|
|
2012-11-19 00:30:30 +00:00
|
|
|
return 0;
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Test if there are bytes available for reading on the specified file descriptor.
|
|
|
|
static int can_read(int fd) {
|
|
|
|
struct timeval can_read_timeout = {0, 0};
|
2012-11-19 00:30:30 +00:00
|
|
|
fd_set fds;
|
2006-01-30 19:53:10 +00:00
|
|
|
|
2012-11-19 00:30:30 +00:00
|
|
|
FD_ZERO(&fds);
|
2005-10-25 11:22:47 +00:00
|
|
|
FD_SET(fd, &fds);
|
2019-11-19 02:34:50 +00:00
|
|
|
return select(fd + 1, &fds, nullptr, nullptr, &can_read_timeout) == 1;
|
2005-10-13 14:08:33 +00:00
|
|
|
}
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2019-09-19 06:32:40 +00:00
|
|
|
/// Test if the specified character in the specified string is backslashed. pos may be at the end of
|
|
|
|
/// the string, which indicates if there is a trailing backslash.
|
|
|
|
static bool is_backslashed(const wcstring &str, size_t pos) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// note pos == str.size() is OK.
|
|
|
|
if (pos > str.size()) return false;
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2013-02-02 22:50:22 +00:00
|
|
|
size_t count = 0, idx = pos;
|
2016-05-03 17:36:22 +00:00
|
|
|
while (idx--) {
|
|
|
|
if (str.at(idx) != L'\\') break;
|
2012-11-19 00:30:30 +00:00
|
|
|
count++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (count % 2) == 1;
|
2006-11-20 02:19:34 +00:00
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
static wchar_t unescaped_quote(const wcstring &str, size_t pos) {
|
2013-02-02 22:50:22 +00:00
|
|
|
wchar_t result = L'\0';
|
2016-05-03 17:36:22 +00:00
|
|
|
if (pos < str.size()) {
|
2013-02-02 22:50:22 +00:00
|
|
|
wchar_t c = str.at(pos);
|
2016-05-03 17:36:22 +00:00
|
|
|
if ((c == L'\'' || c == L'"') && !is_backslashed(str, pos)) {
|
2013-02-02 22:50:22 +00:00
|
|
|
result = c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Returns true if the last token is a comment.
|
|
|
|
static bool text_ends_in_comment(const wcstring &text) {
|
2018-02-24 01:28:12 +00:00
|
|
|
tokenizer_t tok(text.c_str(), TOK_ACCEPT_UNFINISHED | TOK_SHOW_COMMENTS);
|
2019-10-13 23:06:16 +00:00
|
|
|
bool is_comment = false;
|
|
|
|
while (auto token = tok.next()) {
|
|
|
|
is_comment = token->type == token_type_t::comment;
|
2015-05-03 01:22:20 +00:00
|
|
|
}
|
2019-10-13 23:06:16 +00:00
|
|
|
return is_comment;
|
2015-05-03 01:22:20 +00:00
|
|
|
}
|
2006-11-20 02:19:34 +00:00
|
|
|
|
2019-03-15 08:11:15 +00:00
|
|
|
/// \return true if an event is a normal character that should be inserted into the buffer.
|
|
|
|
static bool event_is_normal_char(const char_event_t &evt) {
|
|
|
|
if (!evt.is_char()) return false;
|
|
|
|
auto c = evt.get_char();
|
|
|
|
return !fish_reserved_codepoint(c) && c > 31 && c != 127;
|
|
|
|
}
|
|
|
|
|
2019-03-17 18:44:47 +00:00
|
|
|
/// readline_loop_state_t encapsulates the state used in a readline loop.
|
2020-04-06 21:41:12 +00:00
|
|
|
/// It is always stack allocated transient. This state should not be "publicly visible"; public
|
2019-03-17 18:44:47 +00:00
|
|
|
/// state should be in reader_data_t.
|
|
|
|
struct readline_loop_state_t {
|
|
|
|
/// The last command that was executed.
|
2019-03-17 00:56:35 +00:00
|
|
|
maybe_t<readline_cmd_t> last_cmd{};
|
2019-03-17 18:44:47 +00:00
|
|
|
|
|
|
|
/// If the last command was a yank, the length of yanking that occurred.
|
|
|
|
size_t yank_len{0};
|
|
|
|
|
2020-04-06 21:41:12 +00:00
|
|
|
/// If the last "complete" readline command has inserted text into the command line.
|
|
|
|
bool complete_did_insert{true};
|
2019-03-17 18:44:47 +00:00
|
|
|
|
|
|
|
/// List of completions.
|
2020-01-16 00:13:41 +00:00
|
|
|
completion_list_t comp;
|
2012-12-03 10:25:08 +00:00
|
|
|
|
2019-03-17 18:44:47 +00:00
|
|
|
/// Whether the loop has finished, due to reaching the character limit or through executing a
|
|
|
|
/// command.
|
|
|
|
bool finished{false};
|
2019-03-18 06:37:46 +00:00
|
|
|
|
|
|
|
/// Maximum number of characters to read.
|
|
|
|
size_t nchars{std::numeric_limits<size_t>::max()};
|
2019-03-17 18:44:47 +00:00
|
|
|
};
|
|
|
|
|
2020-11-15 20:26:06 +00:00
|
|
|
/// Run a sequence of commands from an input binding.
|
|
|
|
void reader_data_t::run_input_command_scripts(const wcstring_list_t &cmds) {
|
Do not reset terminal color when donating term for running key bindings
fish maintains two tty modes: one for itself and one for external
commands. The external command mode is also used when executing
fish-script key bindings, which was added in 5f16a299a728 (note that
commit had the wrong issue, the correct issue is #2114).
Prior to this fix, when switching to external modes, we would also reset
the tty's foreground color. This bumped tty's timestamp, causing us to
believe that the tty had been modified, and then repainting the prompt. If
the prompt were multi-line, we would repaint the whole prompt starting
from its second line, leaving a trailing line above it.
It would be reasonable to save the tty timestamp after resetting the
color, but given that using external modes for keybindings is new, it's
better to instead not reset the color in this case. So migrate the color
resetting to only when we run external commands.
Fixes #7722
2021-02-17 19:04:22 +00:00
|
|
|
// Need to donate/steal the tty - see #2114.
|
2021-03-10 19:58:16 +00:00
|
|
|
// Unfortunately this causes us to enable ECHO,
|
|
|
|
// which means if input arrives while we're running a bind function
|
|
|
|
// it will turn up on screen, see #7770.
|
|
|
|
//
|
|
|
|
// What needs to happen is to tell the parser to acquire the terminal
|
|
|
|
// when it's running an external command, but that's a lot more involved.
|
|
|
|
// term_donate();
|
2020-11-15 20:26:06 +00:00
|
|
|
auto last_statuses = parser().get_last_statuses();
|
|
|
|
for (const wcstring &cmd : cmds) {
|
|
|
|
parser().eval(cmd, io_chain_t{});
|
|
|
|
}
|
|
|
|
parser().set_last_statuses(std::move(last_statuses));
|
2021-03-10 19:58:16 +00:00
|
|
|
// term_steal();
|
2020-11-15 20:26:06 +00:00
|
|
|
}
|
|
|
|
|
2019-03-18 06:37:46 +00:00
|
|
|
/// Read normal characters, inserting them into the command line.
|
|
|
|
/// \return the next unhandled event.
|
|
|
|
maybe_t<char_event_t> reader_data_t::read_normal_chars(readline_loop_state_t &rls) {
|
2020-03-04 22:47:18 +00:00
|
|
|
maybe_t<char_event_t> event_needing_handling{};
|
|
|
|
wcstring accumulated_chars;
|
2019-03-18 06:37:46 +00:00
|
|
|
size_t limit = std::min(rls.nchars - command_line.size(), READAHEAD_MAX);
|
2020-11-15 20:26:06 +00:00
|
|
|
|
|
|
|
using command_handler_t = inputter_t::command_handler_t;
|
|
|
|
command_handler_t normal_handler = [this](const wcstring_list_t &cmds) {
|
|
|
|
this->run_input_command_scripts(cmds);
|
|
|
|
};
|
|
|
|
command_handler_t empty_handler = {};
|
|
|
|
|
2020-03-04 22:47:18 +00:00
|
|
|
while (accumulated_chars.size() < limit) {
|
|
|
|
bool allow_commands = (accumulated_chars.empty());
|
2020-11-15 20:26:06 +00:00
|
|
|
auto evt = inputter.readch(allow_commands ? normal_handler : empty_handler);
|
2020-10-31 17:14:50 +00:00
|
|
|
if (!event_is_normal_char(evt) || !can_read(conf.in)) {
|
2020-03-04 22:47:18 +00:00
|
|
|
event_needing_handling = std::move(evt);
|
2019-03-18 06:37:46 +00:00
|
|
|
break;
|
2020-03-07 21:55:19 +00:00
|
|
|
} else if (evt.input_style == char_input_style_t::notfirst && accumulated_chars.empty() &&
|
2020-03-04 22:47:18 +00:00
|
|
|
active_edit_line()->position() == 0) {
|
|
|
|
// The cursor is at the beginning and nothing is accumulated, so skip this character.
|
|
|
|
continue;
|
2019-03-18 06:37:46 +00:00
|
|
|
} else {
|
2020-03-04 22:47:18 +00:00
|
|
|
accumulated_chars.push_back(evt.get_char());
|
2019-03-18 06:37:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-04 22:47:18 +00:00
|
|
|
if (!accumulated_chars.empty()) {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
insert_string(el, accumulated_chars);
|
2019-03-18 06:37:46 +00:00
|
|
|
|
2020-03-04 22:47:18 +00:00
|
|
|
// End paging upon inserting into the normal command line.
|
|
|
|
if (el == &command_line) {
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2020-03-04 22:47:18 +00:00
|
|
|
}
|
2019-03-18 06:37:46 +00:00
|
|
|
|
2020-03-04 22:47:18 +00:00
|
|
|
// Since we handled a normal character, we don't have a last command.
|
|
|
|
rls.last_cmd.reset();
|
|
|
|
}
|
2019-03-18 06:37:46 +00:00
|
|
|
return event_needing_handling;
|
|
|
|
}
|
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
/// Handle a readline command \p c, updating the state \p rls.
|
|
|
|
void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_state_t &rls) {
|
2019-05-27 01:51:26 +00:00
|
|
|
const auto &vars = this->vars();
|
2019-03-19 03:06:16 +00:00
|
|
|
using rl = readline_cmd_t;
|
|
|
|
switch (c) {
|
|
|
|
// Go to beginning of line.
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::beginning_of_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
while (el->position() > 0 && el->text().at(el->position() - 1) != L'\n') {
|
|
|
|
update_buff_pos(el, el->position() - 1);
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
2014-08-19 12:28:08 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::end_of_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() < el->size()) {
|
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
while (buff[el->position()] && buff[el->position()] != L'\n') {
|
|
|
|
update_buff_pos(el, el->position() + 1);
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
accept_autosuggestion(true);
|
2014-08-19 12:28:08 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
2019-03-15 08:11:15 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::beginning_of_buffer: {
|
2019-03-19 03:06:16 +00:00
|
|
|
update_buff_pos(&command_line, 0);
|
|
|
|
break;
|
2019-03-17 00:56:35 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::end_of_buffer: {
|
2019-03-19 03:06:16 +00:00
|
|
|
update_buff_pos(&command_line, command_line.size());
|
|
|
|
break;
|
2019-03-17 00:56:35 +00:00
|
|
|
}
|
2020-07-01 18:54:13 +00:00
|
|
|
case rl::cancel_commandline: {
|
|
|
|
if (command_line.size()) {
|
|
|
|
outputter_t &outp = outputter_t::stdoutput();
|
|
|
|
// Move cursor to the end of the line.
|
|
|
|
update_buff_pos(&command_line, command_line.size());
|
2020-07-01 19:00:06 +00:00
|
|
|
autosuggestion.clear();
|
2020-07-01 18:54:13 +00:00
|
|
|
// Repaint also changes the actual cursor position
|
2020-08-23 22:12:47 +00:00
|
|
|
if (this->is_repaint_needed()) this->layout_and_repaint(L"cancel");
|
2020-07-01 18:54:13 +00:00
|
|
|
|
|
|
|
auto fish_color_cancel = vars.get(L"fish_color_cancel");
|
|
|
|
if (fish_color_cancel) {
|
2020-07-05 06:38:03 +00:00
|
|
|
outp.set_color(parse_color(*fish_color_cancel, false),
|
|
|
|
parse_color(*fish_color_cancel, true));
|
2020-07-01 18:54:13 +00:00
|
|
|
}
|
|
|
|
outp.writestr(L"^C");
|
|
|
|
outp.set_color(rgb_color_t::reset(), rgb_color_t::reset());
|
|
|
|
|
|
|
|
// We print a newline last so the prompt_sp hack doesn't get us.
|
|
|
|
outp.push_back('\n');
|
|
|
|
|
|
|
|
set_command_line_and_position(&command_line, L"", 0);
|
|
|
|
s_reset_abandoning_line(&screen, termsize_last().width - command_line.size());
|
2020-10-07 00:49:07 +00:00
|
|
|
|
|
|
|
// Post fish_cancel, allowing it to fire.
|
|
|
|
signal_clear_cancel();
|
|
|
|
event_fire_generic(parser(), L"fish_cancel");
|
2020-07-01 18:54:13 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::cancel: {
|
2020-10-30 18:29:46 +00:00
|
|
|
// If we last inserted a completion, undo it.
|
|
|
|
// This doesn't apply if the completion was selected via the pager
|
|
|
|
// (in which case the last command is "execute" or similar,
|
|
|
|
// but never complete{,_and_search})
|
|
|
|
//
|
|
|
|
// Also paging is already cancelled above.
|
|
|
|
if (rls.complete_did_insert &&
|
2020-11-22 13:39:48 +00:00
|
|
|
(rls.last_cmd == rl::complete || rls.last_cmd == rl::complete_and_search)) {
|
2020-10-30 18:29:46 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
el->undo();
|
|
|
|
update_buff_pos(el);
|
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
2014-01-20 07:52:35 +00:00
|
|
|
}
|
2019-04-01 13:52:21 +00:00
|
|
|
case rl::repaint_mode: {
|
2019-05-14 11:07:04 +00:00
|
|
|
// Repaint the mode-prompt only if possible.
|
2019-04-01 13:52:21 +00:00
|
|
|
// This is an optimization basically exclusively for vi-mode, since the prompt
|
2019-05-05 10:09:25 +00:00
|
|
|
// may sometimes take a while but when switching the mode all we care about is the
|
|
|
|
// mode-prompt.
|
2019-05-14 11:07:04 +00:00
|
|
|
//
|
2019-05-27 01:51:26 +00:00
|
|
|
// Because some users set `fish_mode_prompt` to an empty function and display the mode
|
|
|
|
// elsewhere, we detect if the mode output is empty.
|
2020-09-11 17:23:26 +00:00
|
|
|
|
|
|
|
// Don't go into an infinite loop of repainting.
|
|
|
|
// This can happen e.g. if a variable triggers a repaint,
|
|
|
|
// and the variable is set inside the prompt (#7324).
|
|
|
|
// builtin commandline will refuse to enqueue these.
|
|
|
|
parser().libdata().is_repaint = true;
|
2019-05-14 11:07:04 +00:00
|
|
|
exec_mode_prompt();
|
2019-05-27 01:51:26 +00:00
|
|
|
if (!mode_prompt_buff.empty()) {
|
2020-06-07 22:16:27 +00:00
|
|
|
s_reset_line(&screen, true /* redraw prompt */);
|
2020-08-23 22:12:47 +00:00
|
|
|
if (this->is_repaint_needed()) this->layout_and_repaint(L"mode");
|
2020-09-11 17:23:26 +00:00
|
|
|
parser().libdata().is_repaint = false;
|
2019-04-01 14:04:14 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-02-06 23:01:17 +00:00
|
|
|
// Else we repaint as normal.
|
|
|
|
__fallthrough__
|
2019-04-01 13:52:21 +00:00
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::force_repaint:
|
|
|
|
case rl::repaint: {
|
2020-09-11 17:23:26 +00:00
|
|
|
parser().libdata().is_repaint = true;
|
2020-08-24 20:19:57 +00:00
|
|
|
exec_prompt();
|
|
|
|
s_reset_line(&screen, true /* redraw prompt */);
|
|
|
|
this->layout_and_repaint(L"readline");
|
|
|
|
force_exec_prompt_and_repaint = false;
|
2020-09-11 17:23:26 +00:00
|
|
|
parser().libdata().is_repaint = false;
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::complete:
|
2019-06-16 21:38:27 +00:00
|
|
|
case rl::complete_and_search: {
|
2020-08-03 21:53:09 +00:00
|
|
|
if (!conf.complete_ok) break;
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// Use the command line only; it doesn't make sense to complete in any other line.
|
|
|
|
editable_line_t *el = &command_line;
|
|
|
|
if (is_navigating_pager_contents() ||
|
2020-09-17 16:35:04 +00:00
|
|
|
(!rls.comp.empty() && !rls.complete_did_insert && rls.last_cmd == rl::complete)) {
|
2019-03-24 00:32:39 +00:00
|
|
|
// The user typed complete more than once in a row. If we are not yet fully
|
2019-03-19 03:06:16 +00:00
|
|
|
// disclosed, then become so; otherwise cycle through our available completions.
|
|
|
|
if (current_page_rendering.remaining_to_disclose > 0) {
|
|
|
|
pager.set_fully_disclosed(true);
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(c == rl::complete ? selection_motion_t::next
|
|
|
|
: selection_motion_t::prev);
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
// Either the user hit tab only once, or we had no visible completion list.
|
|
|
|
// Remove a trailing backslash. This may trigger an extra repaint, but this is
|
|
|
|
// rare.
|
2020-02-04 11:47:44 +00:00
|
|
|
if (is_backslashed(el->text(), el->position())) {
|
2020-02-04 11:47:44 +00:00
|
|
|
delete_char();
|
2012-11-25 05:06:42 +00:00
|
|
|
}
|
2013-05-20 08:40:24 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Get the string; we have to do this after removing any trailing backslash.
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *const buff = el->text().c_str();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Figure out the extent of the command substitution surrounding the cursor.
|
|
|
|
// This is because we only look at the current command substitution to form
|
|
|
|
// completions - stuff happening outside of it is not interesting.
|
|
|
|
const wchar_t *cmdsub_begin, *cmdsub_end;
|
2020-02-04 11:47:44 +00:00
|
|
|
parse_util_cmdsubst_extent(buff, el->position(), &cmdsub_begin, &cmdsub_end);
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Figure out the extent of the token within the command substitution. Note we
|
|
|
|
// pass cmdsub_begin here, not buff.
|
|
|
|
const wchar_t *token_begin, *token_end;
|
2020-02-04 11:47:44 +00:00
|
|
|
parse_util_token_extent(cmdsub_begin, el->position() - (cmdsub_begin - buff),
|
2019-11-19 02:34:50 +00:00
|
|
|
&token_begin, &token_end, nullptr, nullptr);
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Hack: the token may extend past the end of the command substitution, e.g. in
|
|
|
|
// (echo foo) the last token is 'foo)'. Don't let that happen.
|
|
|
|
if (token_end > cmdsub_end) token_end = cmdsub_end;
|
2013-02-03 19:38:22 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Construct a copy of the string from the beginning of the command substitution
|
|
|
|
// up to the end of the token we're completing.
|
|
|
|
const wcstring buffcpy = wcstring(cmdsub_begin, token_end);
|
2013-03-22 00:44:51 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// std::fwprintf(stderr, L"Complete (%ls)\n", buffcpy.c_str());
|
2019-05-05 00:55:57 +00:00
|
|
|
completion_request_flags_t complete_flags = {completion_request_t::descriptions,
|
|
|
|
completion_request_t::fuzzy_match};
|
2020-01-16 01:14:47 +00:00
|
|
|
rls.comp = complete(buffcpy, complete_flags, parser_ref->context());
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-10-19 01:08:22 +00:00
|
|
|
// User-supplied completions may have changed the commandline - prevent buffer
|
|
|
|
// overflow.
|
2020-02-04 11:47:44 +00:00
|
|
|
if (token_begin > buff + el->text().size()) token_begin = buff + el->text().size();
|
|
|
|
if (token_end > buff + el->text().size()) token_end = buff + el->text().size();
|
2019-11-01 12:21:49 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Munge our completions.
|
|
|
|
completions_sort_and_prioritize(&rls.comp);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Record our cycle_command_line.
|
2020-02-04 11:47:44 +00:00
|
|
|
cycle_command_line = el->text();
|
2019-09-09 19:54:44 +00:00
|
|
|
cycle_cursor_pos = token_end - buff;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-04-21 19:41:21 +00:00
|
|
|
rls.complete_did_insert =
|
|
|
|
handle_completions(rls.comp, token_begin - buff, token_end - buff);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Show the search field if requested and if we printed a list of completions.
|
2020-04-06 21:41:12 +00:00
|
|
|
if (c == rl::complete_and_search && !rls.complete_did_insert && !pager.empty()) {
|
2019-03-19 03:06:16 +00:00
|
|
|
pager.set_search_field_shown(true);
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(selection_motion_t::next);
|
2018-01-30 17:39:04 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::pager_toggle_search: {
|
2019-03-19 03:06:16 +00:00
|
|
|
if (!pager.empty()) {
|
|
|
|
// Toggle search, and begin navigating if we are now searching.
|
|
|
|
bool sfs = pager.is_search_field_shown();
|
|
|
|
pager.set_search_field_shown(!sfs);
|
|
|
|
pager.set_fully_disclosed(true);
|
|
|
|
if (pager.is_search_field_shown() && !is_navigating_pager_contents()) {
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(selection_motion_t::south);
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::kill_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
const wchar_t *begin = &buff[el->position()];
|
2019-03-19 03:06:16 +00:00
|
|
|
const wchar_t *end = begin;
|
2013-02-20 01:48:51 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
while (*end && *end != L'\n') end++;
|
2013-02-20 01:48:51 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (end == begin && *end) end++;
|
2012-11-19 08:31:03 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
size_t len = end - begin;
|
|
|
|
if (len) {
|
2019-03-24 00:32:39 +00:00
|
|
|
kill(el, begin - buff, len, KILL_APPEND, rls.last_cmd != rl::kill_line);
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_kill_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() <= 0) {
|
2012-11-19 08:31:03 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
const wchar_t *end = &buff[el->position()];
|
2019-03-19 03:06:16 +00:00
|
|
|
const wchar_t *begin = end;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
begin--; // make sure we delete at least one character (see issue #580)
|
2015-09-19 03:47:38 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Delete until we hit a newline, or the beginning of the string.
|
|
|
|
while (begin > buff && *begin != L'\n') begin--;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// If we landed on a newline, don't delete it.
|
|
|
|
if (*begin == L'\n') begin++;
|
|
|
|
assert(end >= begin);
|
|
|
|
size_t len = std::max<size_t>(end - begin, 1);
|
|
|
|
begin = end - len;
|
2019-03-24 00:32:39 +00:00
|
|
|
kill(el, begin - buff, len, KILL_PREPEND, rls.last_cmd != rl::backward_kill_line);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::kill_whole_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
// We match the emacs behavior here: "kills the entire line including the following
|
|
|
|
// newline".
|
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// Back up to the character just past the previous newline, or go to the beginning
|
|
|
|
// of the command line. Note that if the position is on a newline, visually this
|
|
|
|
// looks like the cursor is at the end of a line. Therefore that newline is NOT the
|
|
|
|
// beginning of a line; this justifies the -1 check.
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t begin = el->position();
|
2019-03-19 03:06:16 +00:00
|
|
|
while (begin > 0 && buff[begin - 1] != L'\n') {
|
|
|
|
begin--;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Push end forwards to just past the next newline, or just past the last char.
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t end = el->position();
|
2019-03-19 03:06:16 +00:00
|
|
|
while (buff[end] != L'\0') {
|
|
|
|
end++;
|
|
|
|
if (buff[end - 1] == L'\n') {
|
|
|
|
break;
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
assert(end >= begin);
|
|
|
|
|
|
|
|
if (end > begin) {
|
2019-03-24 00:32:39 +00:00
|
|
|
kill(el, begin, end - begin, KILL_APPEND, rls.last_cmd != rl::kill_whole_line);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::yank: {
|
2019-03-19 03:06:16 +00:00
|
|
|
wcstring yank_str = kill_yank();
|
|
|
|
insert_string(active_edit_line(), yank_str);
|
|
|
|
rls.yank_len = yank_str.size();
|
|
|
|
break;
|
|
|
|
}
|
2019-09-21 21:31:13 +00:00
|
|
|
case rl::yank_pop: {
|
2019-03-19 03:06:16 +00:00
|
|
|
if (rls.yank_len) {
|
2020-02-04 11:47:44 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2019-03-19 03:06:16 +00:00
|
|
|
wcstring yank_str = kill_yank_rotate();
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t new_yank_len = yank_str.size();
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(el, el->position() - rls.yank_len, rls.yank_len,
|
|
|
|
std::move(yank_str));
|
2020-02-04 11:47:44 +00:00
|
|
|
update_buff_pos(el);
|
|
|
|
rls.yank_len = new_yank_len;
|
|
|
|
suppress_autosuggestion = true;
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_delete_char: {
|
2020-02-04 11:47:44 +00:00
|
|
|
delete_char();
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-01-04 08:45:34 +00:00
|
|
|
case rl::exit: {
|
|
|
|
// This is by definition a successful exit, override the status
|
|
|
|
parser().set_last_statuses(statuses_t::just(STATUS_CMD_OK));
|
|
|
|
exit_loop_requested = true;
|
|
|
|
check_exit_loop_maybe_warning(this);
|
|
|
|
break;
|
|
|
|
}
|
2019-06-26 20:53:19 +00:00
|
|
|
case rl::delete_or_exit:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::delete_char: {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Remove the current character in the character buffer and on the screen using
|
|
|
|
// syntax highlighting, etc.
|
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() < el->size()) {
|
2020-02-04 11:47:44 +00:00
|
|
|
delete_char(false /* backward */);
|
2019-07-03 09:44:08 +00:00
|
|
|
} else if (c == rl::delete_or_exit && el->empty()) {
|
2020-09-16 15:44:43 +00:00
|
|
|
// This is by definition a successful exit, override the status
|
|
|
|
parser().set_last_statuses(statuses_t::just(STATUS_CMD_OK));
|
2020-08-15 21:41:11 +00:00
|
|
|
exit_loop_requested = true;
|
|
|
|
check_exit_loop_maybe_warning(this);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Evaluate. If the current command is unfinished, or if the charater is escaped
|
|
|
|
// using a backslash, insert a newline.
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::execute: {
|
2019-03-19 03:06:16 +00:00
|
|
|
// If the user hits return while navigating the pager, it only clears the pager.
|
|
|
|
if (is_navigating_pager_contents()) {
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2012-11-19 00:30:30 +00:00
|
|
|
break;
|
|
|
|
}
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Delete any autosuggestion.
|
|
|
|
autosuggestion.clear();
|
2016-05-25 19:23:50 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// The user may have hit return with pager contents, but while not navigating them.
|
|
|
|
// Clear the pager in that event.
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2014-03-16 23:49:31 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// We only execute the command line.
|
|
|
|
editable_line_t *el = &command_line;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Allow backslash-escaped newlines.
|
|
|
|
bool continue_on_next_line = false;
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() >= el->size()) {
|
2019-03-19 03:06:16 +00:00
|
|
|
// We're at the end of the text and not in a comment (issue #1225).
|
|
|
|
continue_on_next_line =
|
2020-02-04 11:47:44 +00:00
|
|
|
is_backslashed(el->text(), el->position()) && !text_ends_in_comment(el->text());
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
// Allow mid line split if the following character is whitespace (issue #613).
|
2020-02-04 11:47:44 +00:00
|
|
|
if (is_backslashed(el->text(), el->position()) &&
|
|
|
|
iswspace(el->text().at(el->position()))) {
|
2019-03-19 03:06:16 +00:00
|
|
|
continue_on_next_line = true;
|
|
|
|
// Check if the end of the line is backslashed (issue #4467).
|
2020-02-04 11:47:44 +00:00
|
|
|
} else if (is_backslashed(el->text(), el->size()) &&
|
|
|
|
!text_ends_in_comment(el->text())) {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Move the cursor to the end of the line.
|
2020-02-04 11:47:44 +00:00
|
|
|
el->set_position(el->size());
|
2019-03-19 03:06:16 +00:00
|
|
|
continue_on_next_line = true;
|
2017-10-12 13:07:21 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
// If the conditions are met, insert a new line at the position of the cursor.
|
|
|
|
if (continue_on_next_line) {
|
2019-10-14 08:38:55 +00:00
|
|
|
insert_char(el, L'\n');
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2012-11-19 08:31:03 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// See if this command is valid.
|
2020-08-03 21:22:10 +00:00
|
|
|
parser_test_error_bits_t command_test_result = 0;
|
2020-08-03 21:53:09 +00:00
|
|
|
if (conf.syntax_check_ok) {
|
2020-08-03 21:22:10 +00:00
|
|
|
command_test_result = reader_shell_test(parser(), el->text());
|
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
if (command_test_result == 0 || command_test_result == PARSER_TEST_INCOMPLETE) {
|
|
|
|
// This command is valid, but an abbreviation may make it invalid. If so, we
|
|
|
|
// will have to test again.
|
2020-11-02 02:25:09 +00:00
|
|
|
if (expand_abbreviation_as_necessary(1)) {
|
|
|
|
// Trigger syntax highlighting as we are likely about to execute this command.
|
|
|
|
this->super_highlight_me_plenty();
|
|
|
|
if (conf.syntax_check_ok) {
|
|
|
|
command_test_result = reader_shell_test(parser(), el->text());
|
|
|
|
}
|
2013-07-17 07:38:04 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
2013-07-17 07:38:04 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (command_test_result == 0) {
|
2021-01-02 22:34:19 +00:00
|
|
|
// Finished command, execute it. Don't add items in silent mode (#7230).
|
|
|
|
wcstring text = command_line.text();
|
|
|
|
if (text.empty()) {
|
|
|
|
// Here the user just hit return. Make a new prompt, don't remove ephemeral
|
|
|
|
// items.
|
|
|
|
rls.finished = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Historical behavior is to trim trailing spaces.
|
2021-01-30 23:51:27 +00:00
|
|
|
// However, escaped spaces ('\ ') should not be trimmed (#7661)
|
2021-01-31 00:20:20 +00:00
|
|
|
// This can be done by counting pre-trailing '\'
|
|
|
|
// If there's an odd number, this must be an escaped space.
|
|
|
|
while (!text.empty() && text.back() == L' ' &&
|
|
|
|
count_preceding_backslashes(text, text.size() - 1) % 2 == 0) {
|
|
|
|
text.pop_back();
|
2021-01-02 22:34:19 +00:00
|
|
|
}
|
2021-01-01 21:57:45 +00:00
|
|
|
|
2021-01-02 22:34:19 +00:00
|
|
|
if (history && !conf.in_silent_mode) {
|
|
|
|
// Remove ephemeral items.
|
|
|
|
// Note we fall into this case if the user just types a space and hits return.
|
|
|
|
history->remove_ephemeral_items();
|
|
|
|
|
|
|
|
// Mark this item as ephemeral if there is a leading space (#615).
|
2021-01-01 21:57:45 +00:00
|
|
|
history_persistence_mode_t mode;
|
|
|
|
if (text.front() == L' ') {
|
|
|
|
// Leading spaces are ephemeral (#615).
|
|
|
|
mode = history_persistence_mode_t::ephemeral;
|
|
|
|
} else if (in_private_mode(vars)) {
|
|
|
|
// Private mode means in-memory only.
|
|
|
|
mode = history_persistence_mode_t::memory;
|
|
|
|
} else {
|
|
|
|
mode = history_persistence_mode_t::disk;
|
|
|
|
}
|
2021-01-10 00:22:32 +00:00
|
|
|
history_t::add_pending_with_file_detection(history, text, vars.snapshot(),
|
|
|
|
mode);
|
2012-02-06 19:59:34 +00:00
|
|
|
}
|
2021-01-01 21:57:45 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
rls.finished = true;
|
|
|
|
update_buff_pos(&command_line, command_line.size());
|
|
|
|
} else if (command_test_result == PARSER_TEST_INCOMPLETE) {
|
|
|
|
// We are incomplete, continue editing.
|
2019-10-14 08:38:55 +00:00
|
|
|
insert_char(el, L'\n');
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
// Result must be some combination including an error. The error message will
|
|
|
|
// already be printed, all we need to do is repaint.
|
2020-04-25 07:23:15 +00:00
|
|
|
wcstring_list_t argv(1, el->text());
|
|
|
|
event_fire_generic(parser(), L"fish_posterror", &argv);
|
2020-06-08 02:35:47 +00:00
|
|
|
s_reset_abandoning_line(&screen, termsize_last().width);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-09-27 15:56:47 +00:00
|
|
|
case rl::history_prefix_search_backward:
|
|
|
|
case rl::history_prefix_search_forward:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::history_search_backward:
|
|
|
|
case rl::history_search_forward:
|
2019-09-27 15:56:47 +00:00
|
|
|
case rl::history_token_search_backward:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::history_token_search_forward: {
|
2019-09-27 15:56:47 +00:00
|
|
|
reader_history_search_t::mode_t mode =
|
|
|
|
(c == rl::history_token_search_backward || c == rl::history_token_search_forward)
|
|
|
|
? reader_history_search_t::token
|
2020-11-22 13:39:48 +00:00
|
|
|
: (c == rl::history_prefix_search_backward ||
|
|
|
|
c == rl::history_prefix_search_forward)
|
|
|
|
? reader_history_search_t::prefix
|
|
|
|
: reader_history_search_t::line;
|
2019-09-27 15:56:47 +00:00
|
|
|
|
2020-08-29 08:59:20 +00:00
|
|
|
bool was_active_before = history_search.active();
|
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (history_search.is_at_end()) {
|
|
|
|
const editable_line_t *el = &command_line;
|
2019-09-27 15:56:47 +00:00
|
|
|
if (mode == reader_history_search_t::token) {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Searching by token.
|
|
|
|
const wchar_t *begin, *end;
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr);
|
2019-03-19 03:06:16 +00:00
|
|
|
if (begin) {
|
|
|
|
wcstring token(begin, end);
|
|
|
|
history_search.reset_to_mode(token, history,
|
|
|
|
reader_history_search_t::token);
|
2016-10-23 03:32:25 +00:00
|
|
|
} else {
|
2019-03-19 03:06:16 +00:00
|
|
|
// No current token, refuse to do a token search.
|
|
|
|
history_search.reset();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Searching by line.
|
2020-02-04 11:47:44 +00:00
|
|
|
history_search.reset_to_mode(el->text(), history, mode);
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// Skip the autosuggestion in the history unless it was truncated.
|
2020-11-27 22:41:35 +00:00
|
|
|
const wcstring &suggest = autosuggestion.text;
|
2019-09-27 15:56:47 +00:00
|
|
|
if (!suggest.empty() && !screen.autosuggestion_is_truncated &&
|
|
|
|
mode != reader_history_search_t::prefix) {
|
2019-03-19 03:06:16 +00:00
|
|
|
history_search.add_skip(suggest);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
if (history_search.active()) {
|
|
|
|
history_search_direction_t dir =
|
2019-09-27 15:56:47 +00:00
|
|
|
(c == rl::history_search_backward || c == rl::history_token_search_backward ||
|
|
|
|
c == rl::history_prefix_search_backward)
|
2019-03-19 03:06:16 +00:00
|
|
|
? history_search_direction_t::backward
|
|
|
|
: history_search_direction_t::forward;
|
2020-08-29 08:59:20 +00:00
|
|
|
bool found = history_search.move_in_direction(dir);
|
2020-09-30 15:33:50 +00:00
|
|
|
|
|
|
|
// Signal that we've found nothing
|
|
|
|
if (!found) flash();
|
|
|
|
|
2020-08-29 08:59:20 +00:00
|
|
|
if (!found && !was_active_before) {
|
|
|
|
history_search.reset();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (found ||
|
2020-03-14 19:35:24 +00:00
|
|
|
(dir == history_search_direction_t::forward && history_search.is_at_end())) {
|
|
|
|
update_command_line_from_history_search();
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_char: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (is_navigating_pager_contents()) {
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(selection_motion_t::west);
|
2020-02-04 11:47:44 +00:00
|
|
|
} else if (el->position() > 0) {
|
|
|
|
update_buff_pos(el, el->position() - 1);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::forward_char: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (is_navigating_pager_contents()) {
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(selection_motion_t::east);
|
2020-02-04 11:47:44 +00:00
|
|
|
} else if (el->position() < el->size()) {
|
|
|
|
update_buff_pos(el, el->position() + 1);
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
accept_autosuggestion(true);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-07-21 13:08:38 +00:00
|
|
|
case rl::forward_single_char: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (is_navigating_pager_contents()) {
|
|
|
|
select_completion_in_direction(selection_motion_t::east);
|
|
|
|
} else if (el->position() < el->size()) {
|
|
|
|
update_buff_pos(el, el->position() + 1);
|
|
|
|
} else {
|
|
|
|
accept_autosuggestion(false, true);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_kill_word:
|
|
|
|
case rl::backward_kill_path_component:
|
|
|
|
case rl::backward_kill_bigword: {
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word_style_t style =
|
2020-11-22 13:39:48 +00:00
|
|
|
(c == rl::backward_kill_bigword ? move_word_style_whitespace
|
|
|
|
: c == rl::backward_kill_path_component ? move_word_style_path_components
|
|
|
|
: move_word_style_punctuation);
|
2019-03-19 03:06:16 +00:00
|
|
|
// Is this the same killring item as the last kill?
|
2019-03-24 00:32:39 +00:00
|
|
|
bool newv = (rls.last_cmd != rl::backward_kill_word &&
|
|
|
|
rls.last_cmd != rl::backward_kill_path_component &&
|
|
|
|
rls.last_cmd != rl::backward_kill_bigword);
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word(active_edit_line(), MOVE_DIR_LEFT, true /* erase */, style, newv);
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::kill_word:
|
|
|
|
case rl::kill_bigword: {
|
2019-03-19 03:06:16 +00:00
|
|
|
// The "bigword" functions differ only in that they move to the next whitespace, not
|
|
|
|
// punctuation.
|
|
|
|
auto move_style =
|
2019-03-24 00:32:39 +00:00
|
|
|
(c == rl::kill_word) ? move_word_style_punctuation : move_word_style_whitespace;
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word(active_edit_line(), MOVE_DIR_RIGHT, true /* erase */, move_style,
|
|
|
|
rls.last_cmd != c /* same kill item if same movement */);
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::backward_word:
|
|
|
|
case rl::backward_bigword: {
|
2019-05-05 10:09:25 +00:00
|
|
|
auto move_style =
|
|
|
|
(c == rl::backward_word) ? move_word_style_punctuation : move_word_style_whitespace;
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word(active_edit_line(), MOVE_DIR_LEFT, false /* do not erase */, move_style,
|
|
|
|
false);
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::forward_word:
|
|
|
|
case rl::forward_bigword: {
|
2019-05-05 10:09:25 +00:00
|
|
|
auto move_style =
|
|
|
|
(c == rl::forward_word) ? move_word_style_punctuation : move_word_style_whitespace;
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() < el->size()) {
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word(el, MOVE_DIR_RIGHT, false /* do not erase */, move_style, false);
|
|
|
|
} else {
|
2020-07-21 13:08:38 +00:00
|
|
|
accept_autosuggestion(false, false, move_style);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::beginning_of_history:
|
|
|
|
case rl::end_of_history: {
|
|
|
|
bool up = (c == rl::beginning_of_history);
|
2019-03-19 03:06:16 +00:00
|
|
|
if (is_navigating_pager_contents()) {
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(up ? selection_motion_t::page_north
|
|
|
|
: selection_motion_t::page_south);
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
if (up) {
|
|
|
|
history_search.go_to_beginning();
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2019-03-19 03:06:16 +00:00
|
|
|
history_search.go_to_end();
|
Add 'bigword' vi key bindings
- Add four new functions: forward-bigword, backward-bigword,
kill-bigword, backward-kill-bigword
- Add new enum move_word_style_whitespace and related state machine
method
- Change vi key bindings to operate on bigwords: B, gE, W, E, dW, diW,
daW, dE, dB, dgE, cW, ciW, caW, cE, cB, cgE, yW, yiW, yaW, yE, yB,
ygE
2015-05-30 22:44:25 +00:00
|
|
|
}
|
2020-06-30 20:20:14 +00:00
|
|
|
if (history_search.active()) {
|
|
|
|
update_command_line_from_history_search();
|
|
|
|
}
|
Add 'bigword' vi key bindings
- Add four new functions: forward-bigword, backward-bigword,
kill-bigword, backward-kill-bigword
- Add new enum move_word_style_whitespace and related state machine
method
- Change vi key bindings to operate on bigwords: B, gE, W, E, dW, diW,
daW, dE, dB, dgE, cW, ciW, caW, cE, cB, cgE, yW, yiW, yaW, yE, yB,
ygE
2015-05-30 22:44:25 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::up_line:
|
|
|
|
case rl::down_line: {
|
2019-03-19 03:06:16 +00:00
|
|
|
if (is_navigating_pager_contents()) {
|
|
|
|
// We are already navigating pager contents.
|
2019-04-28 21:06:03 +00:00
|
|
|
selection_motion_t direction;
|
2019-03-24 00:32:39 +00:00
|
|
|
if (c == rl::down_line) {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Down arrow is always south.
|
2019-04-28 21:06:03 +00:00
|
|
|
direction = selection_motion_t::south;
|
2020-08-03 22:41:12 +00:00
|
|
|
} else if (selection_is_at_top(this)) {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Up arrow, but we are in the first column and first row. End navigation.
|
2019-04-28 21:06:03 +00:00
|
|
|
direction = selection_motion_t::deselect;
|
2015-03-02 21:08:29 +00:00
|
|
|
} else {
|
2019-03-19 03:06:16 +00:00
|
|
|
// Up arrow, go north.
|
2019-04-28 21:06:03 +00:00
|
|
|
direction = selection_motion_t::north;
|
2015-03-02 21:08:29 +00:00
|
|
|
}
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Now do the selection.
|
|
|
|
select_completion_in_direction(direction);
|
|
|
|
} else if (!pager.empty()) {
|
|
|
|
// We pressed a direction with a non-empty pager, begin navigation.
|
2019-04-28 21:06:03 +00:00
|
|
|
select_completion_in_direction(c == rl::down_line ? selection_motion_t::south
|
|
|
|
: selection_motion_t::north);
|
2019-03-19 03:06:16 +00:00
|
|
|
} else {
|
|
|
|
// Not navigating the pager contents.
|
|
|
|
editable_line_t *el = active_edit_line();
|
2020-02-04 11:47:44 +00:00
|
|
|
int line_old = parse_util_get_line_from_offset(el->text(), el->position());
|
2019-03-19 03:06:16 +00:00
|
|
|
int line_new;
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2019-03-24 00:32:39 +00:00
|
|
|
if (c == rl::up_line)
|
2019-03-19 03:06:16 +00:00
|
|
|
line_new = line_old - 1;
|
|
|
|
else
|
|
|
|
line_new = line_old + 1;
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
int line_count = parse_util_lineno(el->text().c_str(), el->size()) - 1;
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (line_new >= 0 && line_new <= line_count) {
|
2020-08-11 20:40:42 +00:00
|
|
|
auto indents = parse_util_compute_indents(el->text());
|
|
|
|
size_t base_pos_new = parse_util_get_offset_from_line(el->text(), line_new);
|
|
|
|
size_t base_pos_old = parse_util_get_offset_from_line(el->text(), line_old);
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2020-04-08 23:56:59 +00:00
|
|
|
assert(base_pos_new != static_cast<size_t>(-1) &&
|
|
|
|
base_pos_old != static_cast<size_t>(-1));
|
2020-08-29 09:07:12 +00:00
|
|
|
int indent_old = indents.at(std::min(indents.size() - 1, base_pos_old));
|
|
|
|
int indent_new = indents.at(std::min(indents.size() - 1, base_pos_new));
|
2014-03-31 17:01:39 +00:00
|
|
|
|
2020-08-11 20:40:42 +00:00
|
|
|
size_t line_offset_old = el->position() - base_pos_old;
|
|
|
|
size_t total_offset_new = parse_util_get_offset(
|
2020-02-04 11:47:44 +00:00
|
|
|
el->text(), line_new, line_offset_old - 4 * (indent_new - indent_old));
|
2019-03-19 03:06:16 +00:00
|
|
|
update_buff_pos(el, total_offset_new);
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::suppress_autosuggestion: {
|
2019-03-19 03:06:16 +00:00
|
|
|
suppress_autosuggestion = true;
|
2020-09-26 08:07:53 +00:00
|
|
|
bool success = !autosuggestion.empty();
|
2019-03-19 03:06:16 +00:00
|
|
|
autosuggestion.clear();
|
2020-09-26 08:07:53 +00:00
|
|
|
// Return true if we had a suggestion to clear.
|
|
|
|
inputter.function_set_status(success);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::accept_autosuggestion: {
|
2019-03-19 03:06:16 +00:00
|
|
|
accept_autosuggestion(true);
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::transpose_chars: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (el->size() < 2) {
|
2012-11-19 08:31:03 +00:00
|
|
|
break;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2013-05-20 19:42:34 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// If the cursor is at the end, transpose the last two characters of the line.
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() == el->size()) {
|
|
|
|
update_buff_pos(el, el->position() - 1);
|
2013-05-20 19:42:34 +00:00
|
|
|
}
|
2013-05-27 21:25:55 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Drag the character before the cursor forward over the character at the cursor,
|
|
|
|
// moving the cursor forward as well.
|
2020-02-04 11:47:44 +00:00
|
|
|
if (el->position() > 0) {
|
|
|
|
wcstring local_cmd = el->text();
|
|
|
|
std::swap(local_cmd.at(el->position()), local_cmd.at(el->position() - 1));
|
|
|
|
set_command_line_and_position(el, std::move(local_cmd), el->position() + 1);
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::transpose_words: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
size_t len = el->size();
|
2020-02-04 11:47:44 +00:00
|
|
|
const wchar_t *buff = el->text().c_str();
|
2019-03-19 03:06:16 +00:00
|
|
|
const wchar_t *tok_begin, *tok_end, *prev_begin, *prev_end;
|
2014-01-15 14:07:22 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// If we are not in a token, look for one ahead.
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t buff_pos = el->position();
|
2019-03-19 03:06:16 +00:00
|
|
|
while (buff_pos != len && !iswalnum(buff[buff_pos])) buff_pos++;
|
2013-05-27 21:25:55 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
update_buff_pos(el, buff_pos);
|
2013-05-27 21:25:55 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
parse_util_token_extent(buff, el->position(), &tok_begin, &tok_end, &prev_begin,
|
2019-03-19 03:06:16 +00:00
|
|
|
&prev_end);
|
2013-05-27 21:25:55 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// In case we didn't find a token at or after the cursor...
|
|
|
|
if (tok_begin == &buff[len]) {
|
|
|
|
// ...retry beginning from the previous token.
|
|
|
|
size_t pos = prev_end - &buff[0];
|
|
|
|
parse_util_token_extent(buff, pos, &tok_begin, &tok_end, &prev_begin, &prev_end);
|
2013-09-09 16:46:16 +00:00
|
|
|
}
|
2018-11-25 17:53:43 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// Make sure we have two tokens.
|
|
|
|
if (prev_begin < prev_end && tok_begin < tok_end && tok_begin > prev_begin) {
|
|
|
|
const wcstring prev(prev_begin, prev_end - prev_begin);
|
|
|
|
const wcstring sep(prev_end, tok_begin - prev_end);
|
|
|
|
const wcstring tok(tok_begin, tok_end - tok_begin);
|
|
|
|
const wcstring trail(tok_end, &buff[len] - tok_end);
|
|
|
|
|
|
|
|
// Compose new command line with swapped tokens.
|
|
|
|
wcstring new_buff(buff, prev_begin - buff);
|
|
|
|
new_buff.append(tok);
|
|
|
|
new_buff.append(sep);
|
|
|
|
new_buff.append(prev);
|
|
|
|
new_buff.append(trail);
|
|
|
|
// Put cursor right after the second token.
|
2020-02-04 11:47:44 +00:00
|
|
|
set_command_line_and_position(el, std::move(new_buff), tok_end - buff);
|
2014-01-15 14:07:22 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-04-17 03:24:25 +00:00
|
|
|
case rl::togglecase_char: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
size_t buff_pos = el->position();
|
|
|
|
|
|
|
|
// Check that the cursor is on a character
|
|
|
|
if (buff_pos < el->size()) {
|
|
|
|
wchar_t chr = el->text().at(buff_pos);
|
|
|
|
wcstring replacement;
|
|
|
|
|
|
|
|
// Toggle the case of the current character
|
|
|
|
bool make_uppercase = iswlower(chr);
|
|
|
|
if (make_uppercase) {
|
|
|
|
chr = towupper(chr);
|
|
|
|
} else {
|
|
|
|
chr = tolower(chr);
|
|
|
|
}
|
|
|
|
|
|
|
|
replacement.push_back(chr);
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(el, buff_pos, (size_t)1, std::move(replacement));
|
2020-04-17 03:24:25 +00:00
|
|
|
|
|
|
|
// Restore the buffer position since replace_substring moves
|
|
|
|
// the buffer position ahead of the replaced text.
|
|
|
|
update_buff_pos(el, buff_pos);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rl::togglecase_selection: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
|
|
|
|
// Check that we have an active selection and get the bounds.
|
|
|
|
size_t start, len;
|
|
|
|
if (reader_get_selection(&start, &len)) {
|
|
|
|
size_t buff_pos = el->position();
|
|
|
|
wcstring replacement;
|
|
|
|
|
|
|
|
// Loop through the selected characters and toggle their case.
|
|
|
|
for (size_t pos = start; pos < start + len && pos < el->size(); pos++) {
|
|
|
|
wchar_t chr = el->text().at(pos);
|
|
|
|
|
|
|
|
// Toggle the case of the current character.
|
|
|
|
bool make_uppercase = iswlower(chr);
|
|
|
|
if (make_uppercase) {
|
|
|
|
chr = towupper(chr);
|
|
|
|
} else {
|
|
|
|
chr = tolower(chr);
|
|
|
|
}
|
|
|
|
|
|
|
|
replacement.push_back(chr);
|
|
|
|
}
|
|
|
|
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(el, start, len, std::move(replacement));
|
2020-04-17 03:24:25 +00:00
|
|
|
|
|
|
|
// Restore the buffer position since replace_substring moves
|
|
|
|
// the buffer position ahead of the replaced text.
|
|
|
|
update_buff_pos(el, buff_pos);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::upcase_word:
|
|
|
|
case rl::downcase_word:
|
|
|
|
case rl::capitalize_word: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
// For capitalize_word, whether we've capitalized a character so far.
|
|
|
|
bool capitalized_first = false;
|
|
|
|
|
|
|
|
// We apply the operation from the current location to the end of the word.
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t pos = el->position();
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t init_pos = pos;
|
2019-03-19 03:06:16 +00:00
|
|
|
move_word(el, MOVE_DIR_RIGHT, false, move_word_style_punctuation, false);
|
2020-02-04 11:47:44 +00:00
|
|
|
wcstring replacement;
|
2020-02-04 11:47:44 +00:00
|
|
|
for (; pos < el->position(); pos++) {
|
|
|
|
wchar_t chr = el->text().at(pos);
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// We always change the case; this decides whether we go uppercase (true) or
|
|
|
|
// lowercase (false).
|
|
|
|
bool make_uppercase;
|
2019-03-24 00:32:39 +00:00
|
|
|
if (c == rl::capitalize_word)
|
2019-03-19 03:06:16 +00:00
|
|
|
make_uppercase = !capitalized_first && iswalnum(chr);
|
|
|
|
else
|
2019-03-24 00:32:39 +00:00
|
|
|
make_uppercase = (c == rl::upcase_word);
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// Apply the operation and then record what we did.
|
|
|
|
if (make_uppercase)
|
|
|
|
chr = towupper(chr);
|
|
|
|
else
|
|
|
|
chr = towlower(chr);
|
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
replacement.push_back(chr);
|
2019-03-19 03:06:16 +00:00
|
|
|
capitalized_first = capitalized_first || make_uppercase;
|
|
|
|
}
|
2020-02-09 17:39:14 +00:00
|
|
|
replace_substring(el, init_pos, pos - init_pos, std::move(replacement));
|
2020-02-04 11:47:44 +00:00
|
|
|
update_buff_pos(el);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-08-11 21:29:52 +00:00
|
|
|
case rl::begin_selection: {
|
|
|
|
if (!selection) selection = selection_data_t{};
|
|
|
|
size_t pos = command_line.position();
|
|
|
|
selection->begin = pos;
|
|
|
|
selection->start = pos;
|
|
|
|
selection->stop = pos + 1;
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-08-11 21:29:52 +00:00
|
|
|
|
|
|
|
case rl::end_selection: {
|
|
|
|
selection.reset();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::swap_selection_start_stop: {
|
2020-08-11 21:29:52 +00:00
|
|
|
if (!selection) break;
|
|
|
|
size_t tmp = selection->begin;
|
|
|
|
selection->begin = command_line.position();
|
|
|
|
selection->start = command_line.position();
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
update_buff_pos(el, tmp);
|
|
|
|
break;
|
|
|
|
}
|
2020-08-11 21:29:52 +00:00
|
|
|
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::kill_selection: {
|
|
|
|
bool newv = (rls.last_cmd != rl::kill_selection);
|
2019-03-19 03:06:16 +00:00
|
|
|
size_t start, len;
|
|
|
|
if (reader_get_selection(&start, &len)) {
|
|
|
|
kill(&command_line, start, len, KILL_APPEND, newv);
|
2014-01-18 09:18:29 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-12-19 22:31:33 +00:00
|
|
|
case rl::insert_line_over: {
|
2020-12-19 22:26:13 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
while (el->position() > 0 && el->text().at(el->position() - 1) != L'\n') {
|
|
|
|
update_buff_pos(el, el->position() - 1);
|
|
|
|
}
|
|
|
|
insert_char(el, L'\n');
|
|
|
|
update_buff_pos(el, el->position() - 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rl::insert_line_under: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
if (el->position() < el->size()) {
|
|
|
|
const wchar_t *buff = el->text().c_str();
|
|
|
|
while (buff[el->position()] && buff[el->position()] != L'\n') {
|
|
|
|
update_buff_pos(el, el->position() + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
insert_char(el, L'\n');
|
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::forward_jump:
|
|
|
|
case rl::backward_jump:
|
|
|
|
case rl::forward_jump_till:
|
|
|
|
case rl::backward_jump_till: {
|
|
|
|
auto direction = (c == rl::forward_jump || c == rl::forward_jump_till)
|
2019-03-19 03:06:16 +00:00
|
|
|
? jump_direction_t::forward
|
|
|
|
: jump_direction_t::backward;
|
2019-03-24 00:32:39 +00:00
|
|
|
auto precision = (c == rl::forward_jump || c == rl::backward_jump)
|
2019-03-19 03:06:16 +00:00
|
|
|
? jump_precision_t::to
|
|
|
|
: jump_precision_t::till;
|
|
|
|
editable_line_t *el = active_edit_line();
|
2019-06-02 22:41:23 +00:00
|
|
|
wchar_t target = inputter.function_pop_arg();
|
2019-03-19 03:06:16 +00:00
|
|
|
bool success = jump(direction, precision, el, target);
|
|
|
|
|
2019-06-02 22:41:23 +00:00
|
|
|
inputter.function_set_status(success);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::repeat_jump: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
bool success = false;
|
2018-08-11 07:05:49 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (last_jump_target) {
|
|
|
|
success = jump(last_jump_direction, last_jump_precision, el, last_jump_target);
|
2018-08-11 07:05:49 +00:00
|
|
|
}
|
|
|
|
|
2019-06-02 22:41:23 +00:00
|
|
|
inputter.function_set_status(success);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::reverse_repeat_jump: {
|
2019-03-19 03:06:16 +00:00
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
bool success = false;
|
|
|
|
jump_direction_t original_dir, dir;
|
2020-06-30 22:29:06 +00:00
|
|
|
original_dir = last_jump_direction;
|
2018-08-11 07:05:49 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (last_jump_direction == jump_direction_t::forward) {
|
|
|
|
dir = jump_direction_t::backward;
|
|
|
|
} else {
|
|
|
|
dir = jump_direction_t::forward;
|
2018-08-11 07:05:49 +00:00
|
|
|
}
|
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (last_jump_target) {
|
|
|
|
success = jump(dir, last_jump_precision, el, last_jump_target);
|
|
|
|
}
|
2018-08-11 07:05:49 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
last_jump_direction = original_dir;
|
|
|
|
|
2019-06-02 22:41:23 +00:00
|
|
|
inputter.function_set_status(success);
|
2019-03-19 03:06:16 +00:00
|
|
|
break;
|
|
|
|
}
|
2018-08-11 07:05:49 +00:00
|
|
|
|
2019-04-01 13:59:15 +00:00
|
|
|
case rl::expand_abbr: {
|
|
|
|
if (expand_abbreviation_as_necessary(1)) {
|
2019-06-02 22:41:23 +00:00
|
|
|
inputter.function_set_status(true);
|
2019-04-01 13:59:15 +00:00
|
|
|
} else {
|
2019-06-02 22:41:23 +00:00
|
|
|
inputter.function_set_status(false);
|
2019-04-01 13:59:15 +00:00
|
|
|
}
|
|
|
|
break;
|
2020-02-04 11:47:44 +00:00
|
|
|
}
|
|
|
|
case rl::undo:
|
|
|
|
case rl::redo: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
bool ok = (c == rl::undo) ? el->undo() : el->redo();
|
|
|
|
if (ok) {
|
|
|
|
if (el == &command_line) {
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2020-02-04 11:47:44 +00:00
|
|
|
}
|
|
|
|
update_buff_pos(el);
|
|
|
|
} else {
|
|
|
|
flash();
|
|
|
|
}
|
|
|
|
break;
|
2019-04-01 13:59:15 +00:00
|
|
|
}
|
2021-01-05 21:40:09 +00:00
|
|
|
case rl::begin_undo_group: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
el->begin_edit_group();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rl::end_undo_group: {
|
|
|
|
editable_line_t *el = active_edit_line();
|
|
|
|
el->end_edit_group();
|
|
|
|
break;
|
|
|
|
}
|
2021-02-06 23:13:27 +00:00
|
|
|
case rl::disable_mouse_tracking: {
|
|
|
|
outputter_t &outp = outputter_t::stdoutput();
|
|
|
|
outp.writestr(L"\x1B[?1000l");
|
|
|
|
break;
|
|
|
|
}
|
2021-01-05 21:40:09 +00:00
|
|
|
// Some commands should have been handled internally by inputter_t::readch().
|
2020-03-04 22:10:24 +00:00
|
|
|
case rl::self_insert:
|
|
|
|
case rl::self_insert_notfirst:
|
2020-07-22 17:18:24 +00:00
|
|
|
case rl::func_or:
|
2019-03-24 00:32:39 +00:00
|
|
|
case rl::func_and: {
|
2020-03-04 22:10:24 +00:00
|
|
|
DIE("should have been handled by inputter_t::readch");
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-08-11 07:05:49 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
|
|
|
|
using rl = readline_cmd_t;
|
|
|
|
readline_loop_state_t rls{};
|
|
|
|
|
2019-10-19 01:08:22 +00:00
|
|
|
// Suppress fish_trace during executing key bindings.
|
|
|
|
// This is simply to reduce noise.
|
|
|
|
scoped_push<bool> in_title(&parser().libdata().suppress_fish_trace, true);
|
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
// If nchars_or_0 is positive, then that's the maximum number of chars. Otherwise keep it at
|
|
|
|
// SIZE_MAX.
|
|
|
|
if (nchars_or_0 > 0) {
|
|
|
|
rls.nchars = static_cast<size_t>(nchars_or_0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// The command line before completion.
|
|
|
|
cycle_command_line.clear();
|
|
|
|
cycle_cursor_pos = 0;
|
|
|
|
|
|
|
|
history_search.reset();
|
|
|
|
|
2020-11-27 00:47:08 +00:00
|
|
|
// Get the current terminal modes. These will be restored when the function returns.
|
2020-12-06 14:33:04 +00:00
|
|
|
struct termios old_modes {};
|
2020-11-27 00:47:08 +00:00
|
|
|
if (tcgetattr(conf.in, &old_modes) == -1 && errno == EIO) redirect_tty_output();
|
|
|
|
|
|
|
|
// Set the new modes.
|
|
|
|
if (tcsetattr(conf.in, TCSANOW, &shell_modes) == -1) {
|
|
|
|
int err = errno;
|
|
|
|
if (err == EIO) redirect_tty_output();
|
|
|
|
|
|
|
|
// This check is required to work around certain issues with fish's approach to
|
|
|
|
// terminal control when launching interactive processes while in non-interactive
|
|
|
|
// mode. See #4178 for one such example.
|
2020-12-06 21:40:45 +00:00
|
|
|
if (err != ENOTTY || is_interactive_session()) {
|
2020-11-27 00:47:08 +00:00
|
|
|
wperror(L"tcsetattr");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-08 02:35:47 +00:00
|
|
|
s_reset_abandoning_line(&screen, termsize_last().width);
|
2019-09-30 05:09:26 +00:00
|
|
|
event_fire_generic(parser(), L"fish_prompt");
|
2019-03-19 03:06:16 +00:00
|
|
|
exec_prompt();
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
/// A helper that kicks off syntax highlighting, autosuggestion computing, and repaints.
|
|
|
|
auto color_suggest_repaint_now = [this] {
|
2020-10-31 17:14:50 +00:00
|
|
|
if (conf.in == STDIN_FILENO) {
|
2020-09-30 16:29:00 +00:00
|
|
|
this->update_autosuggestion();
|
|
|
|
this->super_highlight_me_plenty();
|
|
|
|
}
|
2020-08-23 22:12:47 +00:00
|
|
|
if (this->is_repaint_needed()) this->layout_and_repaint(L"toplevel");
|
|
|
|
this->force_exec_prompt_and_repaint = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Start out as initially dirty.
|
|
|
|
force_exec_prompt_and_repaint = true;
|
2019-03-19 03:06:16 +00:00
|
|
|
|
2020-08-15 21:41:11 +00:00
|
|
|
while (!rls.finished && !check_exit_loop_maybe_warning(this)) {
|
2021-02-24 20:00:56 +00:00
|
|
|
if (reset_loop_state) {
|
|
|
|
reset_loop_state = false;
|
|
|
|
rls.last_cmd = none();
|
|
|
|
rls.complete_did_insert = false;
|
|
|
|
}
|
2020-06-08 02:35:47 +00:00
|
|
|
// Perhaps update the termsize. This is cheap if it has not changed.
|
|
|
|
update_termsize();
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
// Repaint as needed.
|
|
|
|
color_suggest_repaint_now();
|
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
if (rls.nchars <= command_line.size()) {
|
|
|
|
// We've already hit the specified character limit.
|
|
|
|
rls.finished = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
maybe_t<char_event_t> event_needing_handling{};
|
2019-11-26 00:36:13 +00:00
|
|
|
while (true) {
|
2019-03-19 03:06:16 +00:00
|
|
|
event_needing_handling = read_normal_chars(rls);
|
|
|
|
if (event_needing_handling.has_value()) break;
|
|
|
|
|
|
|
|
if (rls.nchars <= command_line.size()) {
|
|
|
|
event_needing_handling.reset();
|
2014-01-22 09:00:44 +00:00
|
|
|
break;
|
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!event_needing_handling || event_needing_handling->is_check_exit()) {
|
|
|
|
continue;
|
|
|
|
} else if (event_needing_handling->is_eof()) {
|
2020-08-15 21:41:11 +00:00
|
|
|
reader_sighup();
|
2019-03-19 03:06:16 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
assert((event_needing_handling->is_char() || event_needing_handling->is_readline()) &&
|
|
|
|
"Should have a char or readline");
|
2019-03-17 18:25:16 +00:00
|
|
|
|
2019-09-21 21:31:13 +00:00
|
|
|
if (rls.last_cmd != rl::yank && rls.last_cmd != rl::yank_pop) {
|
2019-03-19 03:06:16 +00:00
|
|
|
rls.yank_len = 0;
|
|
|
|
}
|
|
|
|
|
2019-03-21 04:10:07 +00:00
|
|
|
if (event_needing_handling->is_readline()) {
|
|
|
|
readline_cmd_t readline_cmd = event_needing_handling->get_readline();
|
2019-03-24 00:32:39 +00:00
|
|
|
if (readline_cmd == rl::cancel && is_navigating_pager_contents()) {
|
2020-02-04 11:47:44 +00:00
|
|
|
clear_transient_edit();
|
2019-03-17 18:25:16 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
|
|
|
|
// Clear the pager if necessary.
|
|
|
|
bool focused_on_search_field = (active_edit_line() == &pager.search_field_line);
|
2019-03-21 04:10:07 +00:00
|
|
|
if (command_ends_paging(readline_cmd, focused_on_search_field)) {
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2019-03-17 18:25:16 +00:00
|
|
|
}
|
2019-03-19 03:06:16 +00:00
|
|
|
|
2019-03-21 04:10:07 +00:00
|
|
|
handle_readline_command(readline_cmd, rls);
|
2019-03-19 03:06:16 +00:00
|
|
|
|
2019-06-09 02:06:15 +00:00
|
|
|
if (history_search.active() && command_ends_history_search(readline_cmd)) {
|
2019-05-27 01:51:26 +00:00
|
|
|
// "cancel" means to abort the whole thing, other ending commands mean to finish the
|
|
|
|
// search.
|
2019-06-09 02:06:15 +00:00
|
|
|
if (readline_cmd == rl::cancel) {
|
2020-02-04 11:47:44 +00:00
|
|
|
// Go back to the search string by simply undoing the history-search edit.
|
|
|
|
clear_transient_edit();
|
2019-05-24 17:01:24 +00:00
|
|
|
}
|
2019-05-25 06:31:36 +00:00
|
|
|
history_search.reset();
|
2019-03-17 18:25:16 +00:00
|
|
|
}
|
2019-05-24 17:01:24 +00:00
|
|
|
|
2020-04-29 20:38:15 +00:00
|
|
|
// Readline commands may be bound to \cc which also sets the cancel flag.
|
|
|
|
// See #6937.
|
2020-07-12 18:35:27 +00:00
|
|
|
signal_clear_cancel();
|
2020-04-29 20:38:15 +00:00
|
|
|
|
2019-03-19 03:06:16 +00:00
|
|
|
rls.last_cmd = readline_cmd;
|
2019-03-21 04:10:07 +00:00
|
|
|
} else {
|
|
|
|
// Ordinary char.
|
|
|
|
wchar_t c = event_needing_handling->get_char();
|
2020-03-07 21:55:19 +00:00
|
|
|
if (event_needing_handling->input_style == char_input_style_t::notfirst &&
|
2020-03-04 22:26:04 +00:00
|
|
|
active_edit_line()->position() == 0) {
|
|
|
|
// This character is skipped.
|
|
|
|
} else if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') &&
|
|
|
|
c != 0x7F) {
|
2019-03-17 00:56:35 +00:00
|
|
|
// Regular character.
|
|
|
|
editable_line_t *el = active_edit_line();
|
2019-04-01 13:59:15 +00:00
|
|
|
insert_char(active_edit_line(), c);
|
2019-03-17 00:56:35 +00:00
|
|
|
|
|
|
|
// End paging upon inserting into the normal command line.
|
|
|
|
if (el == &command_line) {
|
2020-08-26 20:43:59 +00:00
|
|
|
pager.clear();
|
2020-02-04 11:47:44 +00:00
|
|
|
command_line_has_transient_edit = false;
|
2019-03-17 00:56:35 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This can happen if the user presses a control char we don't recognize. No
|
|
|
|
// reason to report this to the user unless they've enabled debugging output.
|
2020-01-19 13:27:23 +00:00
|
|
|
FLOGF(reader, _(L"Unknown key binding 0x%X"), c);
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2019-03-21 04:10:07 +00:00
|
|
|
rls.last_cmd = none();
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-23 22:11:42 +00:00
|
|
|
// Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the
|
|
|
|
// user presses enter.
|
2020-11-22 13:39:48 +00:00
|
|
|
if (this->is_repaint_needed() || conf.in != STDIN_FILENO)
|
|
|
|
this->layout_and_repaint(L"prepare to execute");
|
2020-08-23 22:11:42 +00:00
|
|
|
|
2020-11-02 02:25:09 +00:00
|
|
|
// Finish any outstanding syntax highlighting (but do not wait forever).
|
|
|
|
finish_highlighting_before_exec();
|
|
|
|
|
2020-04-28 18:49:26 +00:00
|
|
|
// Emit a newline so that the output is on the line after the command.
|
|
|
|
// But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826.
|
|
|
|
if (!screen.cursor_is_wrapped_to_own_line()) {
|
|
|
|
ignore_result(write(STDOUT_FILENO, "\n", 1));
|
|
|
|
}
|
2013-07-17 09:55:15 +00:00
|
|
|
|
2020-09-30 16:29:00 +00:00
|
|
|
// HACK: If stdin isn't the same terminal as stdout, we just moved the cursor.
|
|
|
|
// For now, just reset it to the beginning of the line.
|
2020-10-31 17:14:50 +00:00
|
|
|
if (conf.in != STDIN_FILENO) {
|
2020-09-30 16:29:00 +00:00
|
|
|
ignore_result(write(STDOUT_FILENO, "\r", 1));
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// Ensure we have no pager contents when we exit.
|
2019-03-03 21:59:35 +00:00
|
|
|
if (!pager.empty()) {
|
2016-05-03 17:36:22 +00:00
|
|
|
// Clear to end of screen to erase the pager contents.
|
|
|
|
// TODO: this may fail if eos doesn't exist, in which case we should emit newlines.
|
2014-01-16 02:21:38 +00:00
|
|
|
screen_force_clear_to_end();
|
2019-03-03 21:59:35 +00:00
|
|
|
pager.clear();
|
2014-01-16 02:21:38 +00:00
|
|
|
}
|
2013-07-17 09:55:15 +00:00
|
|
|
|
2020-08-29 22:14:33 +00:00
|
|
|
if (s_exit_state != exit_state_t::finished_handlers) {
|
2018-09-29 04:13:13 +00:00
|
|
|
// The order of the two conditions below is important. Try to restore the mode
|
|
|
|
// in all cases, but only complain if interactive.
|
2020-12-06 21:40:45 +00:00
|
|
|
if (tcsetattr(conf.in, TCSANOW, &old_modes) == -1 && is_interactive_session()) {
|
2017-01-11 05:52:10 +00:00
|
|
|
if (errno == EIO) redirect_tty_output();
|
2016-05-03 17:36:22 +00:00
|
|
|
wperror(L"tcsetattr"); // return to previous mode
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2018-10-06 20:32:08 +00:00
|
|
|
outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset());
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
return rls.finished ? maybe_t<wcstring>{command_line.text()} : none();
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el,
|
|
|
|
wchar_t target) {
|
2018-08-11 07:05:49 +00:00
|
|
|
bool success = false;
|
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
last_jump_target = target;
|
|
|
|
last_jump_direction = dir;
|
|
|
|
last_jump_precision = precision;
|
2018-08-11 07:05:49 +00:00
|
|
|
|
|
|
|
switch (dir) {
|
|
|
|
case jump_direction_t::backward: {
|
2020-02-04 11:47:44 +00:00
|
|
|
size_t tmp_pos = el->position();
|
2018-08-11 07:05:49 +00:00
|
|
|
|
|
|
|
while (tmp_pos--) {
|
|
|
|
if (el->at(tmp_pos) == target) {
|
|
|
|
if (precision == jump_precision_t::till) {
|
2018-09-09 05:19:05 +00:00
|
|
|
tmp_pos = std::min(el->size() - 1, tmp_pos + 1);
|
2018-08-11 07:05:49 +00:00
|
|
|
}
|
2019-03-03 23:00:29 +00:00
|
|
|
update_buff_pos(el, tmp_pos);
|
2018-08-11 07:05:49 +00:00
|
|
|
success = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case jump_direction_t::forward: {
|
2020-02-04 11:47:44 +00:00
|
|
|
for (size_t tmp_pos = el->position() + 1; tmp_pos < el->size(); tmp_pos++) {
|
2018-08-11 07:05:49 +00:00
|
|
|
if (el->at(tmp_pos) == target) {
|
|
|
|
if (precision == jump_precision_t::till && tmp_pos) {
|
|
|
|
tmp_pos--;
|
|
|
|
}
|
2019-03-03 23:00:29 +00:00
|
|
|
update_buff_pos(el, tmp_pos);
|
2018-08-11 07:05:49 +00:00
|
|
|
success = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
2019-03-03 23:00:29 +00:00
|
|
|
maybe_t<wcstring> reader_readline(int nchars) { return current_data()->readline(nchars); }
|
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
bool reader_is_in_search_mode() {
|
2018-08-11 20:16:07 +00:00
|
|
|
reader_data_t *data = current_data_or_null();
|
2018-08-11 21:30:10 +00:00
|
|
|
return data && data->history_search.active();
|
2007-09-21 14:05:49 +00:00
|
|
|
}
|
|
|
|
|
2018-08-11 21:30:10 +00:00
|
|
|
bool reader_has_pager_contents() {
|
2018-08-11 20:16:07 +00:00
|
|
|
reader_data_t *data = current_data_or_null();
|
2018-08-11 21:30:10 +00:00
|
|
|
return data && !data->current_page_rendering.screen_data.empty();
|
2007-09-21 14:05:49 +00:00
|
|
|
}
|
|
|
|
|
2019-03-03 23:47:00 +00:00
|
|
|
int reader_reading_interrupted() {
|
|
|
|
int res = reader_test_and_clear_interrupted();
|
|
|
|
reader_data_t *data = current_data_or_null();
|
2020-08-03 21:53:09 +00:00
|
|
|
if (res && data && data->conf.exit_on_interrupt) {
|
2020-08-15 21:41:11 +00:00
|
|
|
data->exit_loop_requested = true;
|
2019-03-03 23:47:00 +00:00
|
|
|
// We handled the interrupt ourselves, our caller doesn't need to handle it.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2020-08-23 22:12:47 +00:00
|
|
|
void reader_schedule_prompt_repaint() {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
reader_data_t *data = current_data_or_null();
|
|
|
|
if (data && !data->force_exec_prompt_and_repaint) {
|
|
|
|
data->force_exec_prompt_and_repaint = true;
|
|
|
|
data->inputter.queue_ch(readline_cmd_t::repaint);
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-05 21:40:09 +00:00
|
|
|
void reader_handle_command(readline_cmd_t cmd) {
|
|
|
|
if (reader_data_t *data = current_data_or_null()) {
|
|
|
|
readline_loop_state_t rls{};
|
|
|
|
data->handle_readline_command(cmd, rls);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-02 22:41:23 +00:00
|
|
|
void reader_queue_ch(const char_event_t &ch) {
|
|
|
|
if (reader_data_t *data = current_data_or_null()) {
|
|
|
|
data->inputter.queue_ch(ch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-03 23:47:00 +00:00
|
|
|
const wchar_t *reader_get_buffer() {
|
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
reader_data_t *data = current_data_or_null();
|
2020-02-04 11:47:44 +00:00
|
|
|
return data ? data->command_line.text().c_str() : nullptr;
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 00:22:32 +00:00
|
|
|
std::shared_ptr<history_t> reader_get_history() {
|
2019-03-03 23:47:00 +00:00
|
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
reader_data_t *data = current_data_or_null();
|
2019-11-19 02:34:50 +00:00
|
|
|
return data ? data->history : nullptr;
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the command line contents, clearing the pager.
|
|
|
|
void reader_set_buffer(const wcstring &b, size_t pos) {
|
|
|
|
reader_data_t *data = current_data_or_null();
|
|
|
|
if (!data) return;
|
|
|
|
|
2020-08-26 20:43:59 +00:00
|
|
|
data->pager.clear();
|
2019-03-03 23:47:00 +00:00
|
|
|
data->set_buffer_maintaining_pager(b, pos);
|
2021-02-24 20:00:56 +00:00
|
|
|
data->reset_loop_state = true;
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
size_t reader_get_cursor_pos() {
|
|
|
|
reader_data_t *data = current_data_or_null();
|
2019-11-19 01:08:16 +00:00
|
|
|
if (!data) return static_cast<size_t>(-1);
|
2019-03-03 23:47:00 +00:00
|
|
|
|
2020-02-04 11:47:44 +00:00
|
|
|
return data->command_line.position();
|
2019-03-03 23:47:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool reader_get_selection(size_t *start, size_t *len) {
|
|
|
|
bool result = false;
|
|
|
|
reader_data_t *data = current_data_or_null();
|
2020-08-11 21:29:52 +00:00
|
|
|
if (data != nullptr && data->selection.has_value()) {
|
|
|
|
*start = data->selection->start;
|
|
|
|
*len = std::min(data->selection->stop, data->command_line.size()) - data->selection->start;
|
2019-03-03 23:47:00 +00:00
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
/// Read non-interactively. Read input from stdin without displaying the prompt, using syntax
|
|
|
|
/// highlighting. This is used for reading scripts and init files.
|
2020-01-28 19:39:26 +00:00
|
|
|
/// The file is not closed.
|
2019-05-27 01:51:26 +00:00
|
|
|
static int read_ni(parser_t &parser, int fd, const io_chain_t &io) {
|
2020-01-28 19:39:26 +00:00
|
|
|
// Read all data into a std::string.
|
|
|
|
std::string fd_contents;
|
|
|
|
for (;;) {
|
|
|
|
char buff[4096];
|
2020-05-10 20:29:14 +00:00
|
|
|
ssize_t amt = read(fd, buff, sizeof buff);
|
2020-01-28 19:39:26 +00:00
|
|
|
if (amt > 0) {
|
|
|
|
fd_contents.append(buff, amt);
|
|
|
|
} else if (amt == 0) {
|
|
|
|
// EOF.
|
|
|
|
break;
|
|
|
|
} else {
|
2020-05-10 20:29:14 +00:00
|
|
|
assert(amt == -1);
|
2020-01-28 19:39:26 +00:00
|
|
|
int err = errno;
|
|
|
|
if (err == EINTR) {
|
|
|
|
continue;
|
|
|
|
} else if ((err == EAGAIN || err == EWOULDBLOCK) && make_fd_blocking(fd)) {
|
|
|
|
// We succeeded in making the fd blocking, keep going.
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
// Fatal error.
|
|
|
|
FLOGF(error, _(L"Unable to read input file: %s"), strerror(err));
|
|
|
|
// Reset buffer on error. We won't evaluate incomplete files.
|
|
|
|
fd_contents.clear();
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
2020-01-28 19:39:26 +00:00
|
|
|
}
|
2012-12-19 21:31:06 +00:00
|
|
|
|
2020-01-28 19:39:26 +00:00
|
|
|
wcstring str = str2wcstring(fd_contents);
|
2005-09-20 13:26:39 +00:00
|
|
|
|
2020-01-28 19:39:26 +00:00
|
|
|
// Eagerly deallocate to save memory.
|
|
|
|
fd_contents.clear();
|
|
|
|
fd_contents.shrink_to_fit();
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-01-28 19:39:26 +00:00
|
|
|
// Swallow a BOM (issue #1518).
|
|
|
|
if (!str.empty() && str.at(0) == UTF8_BOM_WCHAR) {
|
|
|
|
str.erase(0, 1);
|
|
|
|
}
|
2014-11-01 23:25:28 +00:00
|
|
|
|
2020-07-12 20:55:51 +00:00
|
|
|
// Parse into an ast and detect errors.
|
2020-01-28 19:39:26 +00:00
|
|
|
parse_error_list_t errors;
|
2020-07-12 20:55:51 +00:00
|
|
|
auto ast = ast::ast_t::parse(str, parse_flag_none, &errors);
|
|
|
|
bool errored = ast.errored();
|
|
|
|
if (!errored) {
|
|
|
|
errored = parse_util_detect_errors(ast, str, &errors);
|
|
|
|
}
|
|
|
|
if (!errored) {
|
|
|
|
// Construct a parsed source ref.
|
|
|
|
// Be careful to transfer ownership, this could be a very large string.
|
|
|
|
parsed_source_ref_t ps = std::make_shared<parsed_source_t>(std::move(str), std::move(ast));
|
|
|
|
parser.eval(ps, io);
|
2020-01-28 19:39:26 +00:00
|
|
|
return 0;
|
2016-05-03 17:36:22 +00:00
|
|
|
} else {
|
2020-01-28 19:39:26 +00:00
|
|
|
wcstring sb;
|
|
|
|
parser.get_backtrace(str, errors, sb);
|
|
|
|
std::fwprintf(stderr, L"%ls", sb.c_str());
|
|
|
|
return 1;
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-27 01:51:26 +00:00
|
|
|
int reader_read(parser_t &parser, int fd, const io_chain_t &io) {
|
2012-11-19 00:30:30 +00:00
|
|
|
int res;
|
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// If reader_read is called recursively through the '.' builtin, we need to preserve
|
|
|
|
// is_interactive. This, and signal handler setup is handled by
|
|
|
|
// proc_push_interactive/proc_pop_interactive.
|
2019-05-27 21:52:48 +00:00
|
|
|
bool interactive = false;
|
2017-01-11 05:52:10 +00:00
|
|
|
// This block is a hack to work around https://sourceware.org/bugzilla/show_bug.cgi?id=20632.
|
|
|
|
// See also, commit 396bf12. Without the need for this workaround we would just write:
|
|
|
|
// int inter = ((fd == STDIN_FILENO) && isatty(STDIN_FILENO));
|
|
|
|
if (fd == STDIN_FILENO) {
|
|
|
|
struct termios t;
|
|
|
|
int a_tty = isatty(STDIN_FILENO);
|
|
|
|
if (a_tty) {
|
2019-05-27 21:52:48 +00:00
|
|
|
interactive = true;
|
2017-01-11 05:52:10 +00:00
|
|
|
} else if (tcgetattr(STDIN_FILENO, &t) == -1 && errno == EIO) {
|
|
|
|
redirect_tty_output();
|
2019-05-27 21:52:48 +00:00
|
|
|
interactive = true;
|
2017-01-11 05:52:10 +00:00
|
|
|
}
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-05-27 21:52:48 +00:00
|
|
|
scoped_push<bool> interactive_push{&parser.libdata().is_interactive, interactive};
|
|
|
|
signal_set_handlers_once(interactive);
|
|
|
|
|
2020-01-28 19:39:26 +00:00
|
|
|
res = interactive ? read_i(parser) : read_ni(parser, fd, io);
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-03 17:36:22 +00:00
|
|
|
// If the exit command was called in a script, only exit the script, not the program.
|
2020-08-15 21:41:11 +00:00
|
|
|
parser.libdata().exit_current_script = false;
|
2012-11-19 00:30:30 +00:00
|
|
|
|
|
|
|
return res;
|
2005-09-20 13:26:39 +00:00
|
|
|
}
|