mirror of
https://github.com/NixOS/nix
synced 2025-06-30 03:23:16 +02:00
Previously, when users entered an incomplete expression in the REPL, the continuation prompt was just 10 blank spaces, which looked invisible and gave the impression that the REPL had stalled. This change updates the prompt to " > ", aligning it visually with 'nix-repl> ' and clearly indicating that the REPL is waiting for more input. Fixes: https://github.com/NixOS/nix/issues/12702
208 lines
5.5 KiB
C++
208 lines
5.5 KiB
C++
#include "cmd-config-private.hh"
|
|
|
|
#include <cstdio>
|
|
|
|
#ifdef USE_READLINE
|
|
#include <readline/history.h>
|
|
#include <readline/readline.h>
|
|
#else
|
|
// editline < 1.15.2 don't wrap their API for C++ usage
|
|
// (added in https://github.com/troglobit/editline/commit/91398ceb3427b730995357e9d120539fb9bb7461).
|
|
// This results in linker errors due to to name-mangling of editline C symbols.
|
|
// For compatibility with these versions, we wrap the API here
|
|
// (wrapping multiple times on newer versions is no problem).
|
|
extern "C" {
|
|
#include <editline.h>
|
|
}
|
|
#endif
|
|
|
|
#include "nix/signals.hh"
|
|
#include "nix/finally.hh"
|
|
#include "nix/repl-interacter.hh"
|
|
#include "nix/file-system.hh"
|
|
#include "nix/repl.hh"
|
|
#include "nix/environment-variables.hh"
|
|
|
|
namespace nix {
|
|
|
|
namespace {
|
|
// Used to communicate to NixRepl::getLine whether a signal occurred in ::readline.
|
|
volatile sig_atomic_t g_signal_received = 0;
|
|
|
|
void sigintHandler(int signo)
|
|
{
|
|
g_signal_received = signo;
|
|
}
|
|
};
|
|
|
|
static detail::ReplCompleterMixin * curRepl; // ugly
|
|
|
|
#ifndef USE_READLINE
|
|
static char * completionCallback(char * s, int * match)
|
|
{
|
|
auto possible = curRepl->completePrefix(s);
|
|
if (possible.size() == 1) {
|
|
*match = 1;
|
|
auto * res = strdup(possible.begin()->c_str() + strlen(s));
|
|
if (!res)
|
|
throw Error("allocation failure");
|
|
return res;
|
|
} else if (possible.size() > 1) {
|
|
auto checkAllHaveSameAt = [&](size_t pos) {
|
|
auto & first = *possible.begin();
|
|
for (auto & p : possible) {
|
|
if (p.size() <= pos || p[pos] != first[pos])
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
size_t start = strlen(s);
|
|
size_t len = 0;
|
|
while (checkAllHaveSameAt(start + len))
|
|
++len;
|
|
if (len > 0) {
|
|
*match = 1;
|
|
auto * res = strdup(std::string(*possible.begin(), start, len).c_str());
|
|
if (!res)
|
|
throw Error("allocation failure");
|
|
return res;
|
|
}
|
|
}
|
|
|
|
*match = 0;
|
|
return nullptr;
|
|
}
|
|
|
|
static int listPossibleCallback(char * s, char *** avp)
|
|
{
|
|
auto possible = curRepl->completePrefix(s);
|
|
|
|
if (possible.size() > (std::numeric_limits<int>::max() / sizeof(char *)))
|
|
throw Error("too many completions");
|
|
|
|
int ac = 0;
|
|
char ** vp = nullptr;
|
|
|
|
auto check = [&](auto * p) {
|
|
if (!p) {
|
|
if (vp) {
|
|
while (--ac >= 0)
|
|
free(vp[ac]);
|
|
free(vp);
|
|
}
|
|
throw Error("allocation failure");
|
|
}
|
|
return p;
|
|
};
|
|
|
|
vp = check((char **) malloc(possible.size() * sizeof(char *)));
|
|
|
|
for (auto & p : possible)
|
|
vp[ac++] = check(strdup(p.c_str()));
|
|
|
|
*avp = vp;
|
|
|
|
return ac;
|
|
}
|
|
#endif
|
|
|
|
ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
|
{
|
|
// Allow nix-repl specific settings in .inputrc
|
|
rl_readline_name = "nix-repl";
|
|
try {
|
|
createDirs(dirOf(historyFile));
|
|
} catch (SystemError & e) {
|
|
logWarning(e.info());
|
|
}
|
|
#ifndef USE_READLINE
|
|
el_hist_size = 1000;
|
|
#endif
|
|
read_history(historyFile.c_str());
|
|
auto oldRepl = curRepl;
|
|
curRepl = repl;
|
|
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
|
|
#ifndef USE_READLINE
|
|
rl_set_complete_func(completionCallback);
|
|
rl_set_list_possib_func(listPossibleCallback);
|
|
#endif
|
|
return restoreRepl;
|
|
}
|
|
|
|
static constexpr const char * promptForType(ReplPromptType promptType)
|
|
{
|
|
switch (promptType) {
|
|
case ReplPromptType::ReplPrompt:
|
|
return "nix-repl> ";
|
|
case ReplPromptType::ContinuationPrompt:
|
|
return " > "; // 9 spaces + >
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptType)
|
|
{
|
|
#ifndef _WIN32 // TODO use more signals.hh for this
|
|
struct sigaction act, old;
|
|
sigset_t savedSignalMask, set;
|
|
|
|
auto setupSignals = [&]() {
|
|
act.sa_handler = sigintHandler;
|
|
sigfillset(&act.sa_mask);
|
|
act.sa_flags = 0;
|
|
if (sigaction(SIGINT, &act, &old))
|
|
throw SysError("installing handler for SIGINT");
|
|
|
|
sigemptyset(&set);
|
|
sigaddset(&set, SIGINT);
|
|
if (sigprocmask(SIG_UNBLOCK, &set, &savedSignalMask))
|
|
throw SysError("unblocking SIGINT");
|
|
};
|
|
auto restoreSignals = [&]() {
|
|
if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
|
|
throw SysError("restoring signals");
|
|
|
|
if (sigaction(SIGINT, &old, 0))
|
|
throw SysError("restoring handler for SIGINT");
|
|
};
|
|
|
|
setupSignals();
|
|
#endif
|
|
char * s = readline(promptForType(promptType));
|
|
Finally doFree([&]() { free(s); });
|
|
#ifndef _WIN32 // TODO use more signals.hh for this
|
|
restoreSignals();
|
|
#endif
|
|
|
|
if (g_signal_received) {
|
|
g_signal_received = 0;
|
|
input.clear();
|
|
return true;
|
|
}
|
|
|
|
// editline doesn't echo the input to the output when non-interactive, unlike readline
|
|
// this results in a different behavior when running tests. The echoing is
|
|
// quite useful for reading the test output, so we add it here.
|
|
if (auto e = getEnv("_NIX_TEST_REPL_ECHO"); s && e && *e == "1")
|
|
{
|
|
#ifndef USE_READLINE
|
|
// This is probably not right for multi-line input, but we don't use that
|
|
// in the characterisation tests, so it's fine.
|
|
std::cout << promptForType(promptType) << s << std::endl;
|
|
#endif
|
|
}
|
|
|
|
if (!s)
|
|
return false;
|
|
input += s;
|
|
input += '\n';
|
|
|
|
return true;
|
|
}
|
|
|
|
ReadlineLikeInteracter::~ReadlineLikeInteracter()
|
|
{
|
|
write_history(historyFile.c_str());
|
|
}
|
|
|
|
};
|